497 lines
12 KiB
TypeScript
497 lines
12 KiB
TypeScript
import React from "react";
|
|
import { cn } from "~/lib/utils";
|
|
import {
|
|
colors,
|
|
typography,
|
|
spacing,
|
|
borderRadius,
|
|
shadows,
|
|
} from "~/lib/design-tokens";
|
|
|
|
// Button Component with Figma variants
|
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive";
|
|
size?: "sm" | "md" | "lg";
|
|
fullWidth?: boolean;
|
|
loading?: boolean;
|
|
leftIcon?: React.ReactNode;
|
|
rightIcon?: React.ReactNode;
|
|
}
|
|
|
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
(
|
|
{
|
|
className,
|
|
variant = "primary",
|
|
size = "md",
|
|
fullWidth = false,
|
|
loading = false,
|
|
leftIcon,
|
|
rightIcon,
|
|
children,
|
|
disabled,
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const baseStyles =
|
|
"inline-flex items-center justify-center font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed font-persian";
|
|
|
|
const variants = {
|
|
primary:
|
|
"bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-primary active:bg-primary/80",
|
|
secondary:
|
|
"bg-secondary text-secondary-foreground hover:bg-secondary/90 focus:ring-secondary active:bg-secondary/80",
|
|
teal: "bg-teal-500 text-slate-800 hover:bg-teal-600 focus:ring-teal-500 active:bg-teal-700",
|
|
outline:
|
|
"border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground focus:ring-ring",
|
|
ghost:
|
|
"text-foreground hover:bg-accent hover:text-accent-foreground focus:ring-ring",
|
|
destructive:
|
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90 focus:ring-destructive active:bg-destructive/80",
|
|
};
|
|
|
|
const sizes = {
|
|
sm: "h-8 px-3 text-sm rounded-md",
|
|
md: "h-10 px-4 text-sm rounded-lg",
|
|
lg: "h-12 px-6 text-base rounded-lg",
|
|
};
|
|
|
|
return (
|
|
<button
|
|
ref={ref}
|
|
className={cn(
|
|
baseStyles,
|
|
variants[variant],
|
|
sizes[size],
|
|
fullWidth && "w-full",
|
|
className,
|
|
)}
|
|
disabled={disabled || loading}
|
|
{...props}
|
|
>
|
|
{loading && (
|
|
<svg
|
|
className="animate-spin -ml-1 mr-2 h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
)}
|
|
{leftIcon && <span className="ml-2">{leftIcon}</span>}
|
|
{children}
|
|
{rightIcon && <span className="mr-2">{rightIcon}</span>}
|
|
</button>
|
|
);
|
|
},
|
|
);
|
|
|
|
Button.displayName = "Button";
|
|
|
|
// Input Component
|
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
label?: string;
|
|
error?: string;
|
|
helper?: string;
|
|
leftIcon?: React.ReactNode;
|
|
rightIcon?: React.ReactNode;
|
|
inputSize?: "sm" | "md" | "lg";
|
|
}
|
|
|
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
(
|
|
{
|
|
className,
|
|
label,
|
|
error,
|
|
helper,
|
|
leftIcon,
|
|
rightIcon,
|
|
inputSize = "md",
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const baseStyles =
|
|
"w-full border rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed font-persian text-right";
|
|
|
|
const sizes = {
|
|
sm: "h-8 px-3 text-sm",
|
|
md: "h-10 px-3 text-sm",
|
|
lg: "h-12 px-4 text-base",
|
|
};
|
|
|
|
const variants = error
|
|
? "border-destructive focus:border-destructive focus:ring-destructive/20"
|
|
: "border-input focus:border-primary focus:ring-primary/20";
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{label && (
|
|
<label className="block text-sm font-medium text-foreground font-persian">
|
|
{label}
|
|
</label>
|
|
)}
|
|
<div className="relative">
|
|
{leftIcon && (
|
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground">
|
|
{leftIcon}
|
|
</div>
|
|
)}
|
|
<input
|
|
ref={ref}
|
|
className={cn(
|
|
baseStyles,
|
|
sizes[inputSize],
|
|
variants,
|
|
leftIcon && "pr-10",
|
|
rightIcon && "pl-10",
|
|
"bg-background text-foreground",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
{rightIcon && (
|
|
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground">
|
|
{rightIcon}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{error && (
|
|
<p className="text-sm text-destructive font-persian">{error}</p>
|
|
)}
|
|
{helper && !error && (
|
|
<p className="text-sm text-muted-foreground font-persian">{helper}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
Input.displayName = "Input";
|
|
|
|
// Card Component
|
|
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
variant?: "default" | "bordered" | "shadow" | "elevated";
|
|
padding?: "none" | "sm" | "md" | "lg";
|
|
}
|
|
|
|
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
(
|
|
{ className, variant = "default", padding = "md", children, ...props },
|
|
ref,
|
|
) => {
|
|
const baseStyles = "rounded-lg bg-card";
|
|
|
|
const variants = {
|
|
default: "border border-border",
|
|
bordered: "border-2 border-border",
|
|
shadow: "shadow-md border border-border",
|
|
elevated: "shadow-lg border border-border",
|
|
};
|
|
|
|
const paddings = {
|
|
none: "",
|
|
sm: "p-4",
|
|
md: "p-6",
|
|
lg: "p-8",
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
baseStyles,
|
|
variants[variant],
|
|
paddings[padding],
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
Card.displayName = "Card";
|
|
|
|
// Badge Component
|
|
interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
variant?:
|
|
| "default"
|
|
| "primary"
|
|
| "secondary"
|
|
| "success"
|
|
| "warning"
|
|
| "error";
|
|
size?: "sm" | "md" | "lg";
|
|
}
|
|
|
|
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
(
|
|
{ className, variant = "default", size = "md", children, ...props },
|
|
ref,
|
|
) => {
|
|
const baseStyles =
|
|
"inline-flex items-center font-medium rounded-full font-persian";
|
|
|
|
const variants = {
|
|
default: "bg-secondary text-secondary-foreground",
|
|
primary: "bg-primary/10 text-primary",
|
|
secondary: "bg-secondary/10 text-secondary",
|
|
success:
|
|
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
warning:
|
|
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
|
error: "bg-destructive/10 text-destructive",
|
|
};
|
|
|
|
const sizes = {
|
|
sm: "px-2 py-0.5 text-xs",
|
|
md: "px-2.5 py-0.5 text-sm",
|
|
lg: "px-3 py-1 text-sm",
|
|
};
|
|
|
|
return (
|
|
<span
|
|
ref={ref}
|
|
className={cn(baseStyles, variants[variant], sizes[size], className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</span>
|
|
);
|
|
},
|
|
);
|
|
|
|
Badge.displayName = "Badge";
|
|
|
|
// Avatar Component
|
|
interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
src?: string;
|
|
alt?: string;
|
|
size?: "sm" | "md" | "lg" | "xl";
|
|
fallback?: string;
|
|
}
|
|
|
|
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
({ className, src, alt, size = "md", fallback, ...props }, ref) => {
|
|
const sizes = {
|
|
sm: "h-8 w-8",
|
|
md: "h-10 w-10",
|
|
lg: "h-12 w-12",
|
|
xl: "h-16 w-16",
|
|
};
|
|
|
|
const textSizes = {
|
|
sm: "text-xs",
|
|
md: "text-sm",
|
|
lg: "text-base",
|
|
xl: "text-lg",
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"relative inline-flex items-center justify-center rounded-full bg-muted",
|
|
sizes[size],
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{src ? (
|
|
<img
|
|
src={src}
|
|
alt={alt}
|
|
className="h-full w-full rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<span
|
|
className={cn(
|
|
"font-medium text-muted-foreground font-persian",
|
|
textSizes[size],
|
|
)}
|
|
>
|
|
{fallback}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
Avatar.displayName = "Avatar";
|
|
|
|
// Loading Spinner Component
|
|
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
size?: "sm" | "md" | "lg";
|
|
color?: "primary" | "secondary" | "white";
|
|
}
|
|
|
|
export const Spinner = React.forwardRef<HTMLDivElement, SpinnerProps>(
|
|
({ className, size = "md", color = "primary", ...props }, ref) => {
|
|
const sizes = {
|
|
sm: "h-4 w-4",
|
|
md: "h-6 w-6",
|
|
lg: "h-8 w-8",
|
|
};
|
|
|
|
const colors = {
|
|
primary: "text-primary",
|
|
secondary: "text-secondary",
|
|
white: "text-white",
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn("animate-spin", sizes[size], colors[color], className)}
|
|
{...props}
|
|
>
|
|
<svg fill="none" viewBox="0 0 24 24">
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
Spinner.displayName = "Spinner";
|
|
|
|
// Progress Bar Component
|
|
interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
value: number;
|
|
max?: number;
|
|
size?: "sm" | "md" | "lg";
|
|
variant?: "default" | "success" | "warning" | "error";
|
|
showLabel?: boolean;
|
|
}
|
|
|
|
export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
|
(
|
|
{
|
|
className,
|
|
value,
|
|
max = 100,
|
|
size = "md",
|
|
variant = "default",
|
|
showLabel = false,
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
|
|
|
const sizes = {
|
|
sm: "h-1",
|
|
md: "h-2",
|
|
lg: "h-3",
|
|
};
|
|
|
|
const variants = {
|
|
default: "bg-primary",
|
|
success: "bg-green-500",
|
|
warning: "bg-yellow-500",
|
|
error: "bg-destructive",
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{showLabel && (
|
|
<div className="flex justify-between text-sm text-muted-foreground font-persian">
|
|
<span>پیشرفت</span>
|
|
<span>{Math.round(percentage)}%</span>
|
|
</div>
|
|
)}
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"w-full bg-secondary rounded-full",
|
|
sizes[size],
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"h-full rounded-full transition-all duration-300",
|
|
variants[variant],
|
|
)}
|
|
style={{ width: `${percentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
Progress.displayName = "Progress";
|
|
|
|
// Divider Component
|
|
interface DividerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
orientation?: "horizontal" | "vertical";
|
|
variant?: "solid" | "dashed" | "dotted";
|
|
}
|
|
|
|
export const Divider = React.forwardRef<HTMLDivElement, DividerProps>(
|
|
(
|
|
{ className, orientation = "horizontal", variant = "solid", ...props },
|
|
ref,
|
|
) => {
|
|
const baseStyles = "border-border";
|
|
|
|
const orientations = {
|
|
horizontal: "w-full border-t",
|
|
vertical: "h-full border-l",
|
|
};
|
|
|
|
const variants = {
|
|
solid: "border-solid",
|
|
dashed: "border-dashed",
|
|
dotted: "border-dotted",
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
baseStyles,
|
|
orientations[orientation],
|
|
variants[variant],
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
},
|
|
);
|
|
|
|
Divider.displayName = "Divider";
|