inogen/app/components/ui/design-system.tsx

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