diff --git a/client/src/components/ui-new/badge.tsx b/client/src/components/ui-new/badge.tsx new file mode 100644 index 0000000..05087d7 --- /dev/null +++ b/client/src/components/ui-new/badge.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "./button" + +/** + * Badge Component Variants + * Default, Primary, Success, Warning, Error, Info variants + */ +const badgeVariants = cva( + "inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-full font-medium transition-all duration-200", + { + variants: { + variant: { + default: + "bg-slate-700 text-slate-200 hover:bg-slate-600", + primary: + "bg-[#3B82F6]/20 text-[#3B82F6] hover:bg-[#3B82F6]/30 border border-[#3B82F6]/30", + success: + "bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 border border-emerald-500/30", + warning: + "bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30", + error: + "bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/30", + info: + "bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/30 border border-cyan-500/30", + }, + size: { + sm: "px-2 py-0.5 text-xs", + md: "px-3 py-1 text-sm", + lg: "px-4 py-1.5 text-base", + }, + }, + defaultVariants: { + variant: "default", + size: "md", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { + icon?: React.ReactNode +} + +const Badge = React.forwardRef( + ({ className, variant, size, icon, children, ...props }, ref) => ( + + {icon && {icon}} + {children} + + ) +) +Badge.displayName = "Badge" + +export { Badge, badgeVariants } diff --git a/client/src/components/ui-new/button.tsx b/client/src/components/ui-new/button.tsx new file mode 100644 index 0000000..5c4849f --- /dev/null +++ b/client/src/components/ui-new/button.tsx @@ -0,0 +1,94 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +/** + * Utility function to merge Tailwind classes + */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +/** + * Button Component Variants + * Primary, Secondary, Ghost, and Icon variants + */ +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + primary: + "bg-[#3B82F6] text-white hover:bg-[#2563EB] active:bg-[#1D4ED8] shadow-md hover:shadow-lg focus-visible:ring-[#3B82F6]", + secondary: + "border border-[#3B82F6] bg-transparent text-[#3B82F6] hover:bg-[#3B82F6]/10 active:bg-[#3B82F6]/20 focus-visible:ring-[#3B82F6]", + ghost: + "bg-transparent text-slate-400 hover:bg-slate-800 hover:text-slate-200 focus-visible:ring-slate-400", + danger: + "bg-red-600 text-white hover:bg-red-700 active:bg-red-800 shadow-md hover:shadow-lg focus-visible:ring-red-600", + outline: + "border border-slate-600 bg-transparent text-slate-300 hover:bg-slate-800 hover:text-slate-200 focus-visible:ring-slate-400", + }, + size: { + sm: "h-8 px-3 text-sm", + md: "h-10 px-4 text-base", + lg: "h-12 px-6 text-lg", + icon: "h-10 w-10 p-2", + "icon-sm": "h-8 w-8 p-1.5", + "icon-lg": "h-12 w-12 p-2.5", + }, + }, + defaultVariants: { + variant: "primary", + size: "md", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean + loading?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, loading, disabled, children, ...props }, ref) => { + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/client/src/components/ui-new/card.tsx b/client/src/components/ui-new/card.tsx new file mode 100644 index 0000000..3f796f4 --- /dev/null +++ b/client/src/components/ui-new/card.tsx @@ -0,0 +1,118 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "./button" + +/** + * Card Component Variants + * Three-tier hierarchy: Primary, Secondary, Tertiary + */ +const cardVariants = cva( + "rounded-xl border transition-all duration-200", + { + variants: { + variant: { + // Primary: Critical information with accent + primary: + "bg-slate-800/90 border-slate-700 shadow-lg backdrop-blur-sm border-l-4 border-l-[#3B82F6]", + // Secondary: Supporting information + secondary: + "bg-slate-800/70 border-slate-700/50 shadow-md backdrop-blur-sm", + // Tertiary: Background containers + tertiary: + "bg-slate-900/50 border-slate-700/30", + // Glass: Glassmorphism effect + glass: + "bg-slate-800/40 backdrop-blur-xl border-white/10 shadow-xl hover:bg-slate-800/60 hover:border-white/20", + // Interactive: Hover effects + interactive: + "bg-slate-800/80 border-slate-700 cursor-pointer hover:bg-slate-800 hover:border-slate-600 hover:shadow-lg transition-all", + }, + padding: { + none: "", + sm: "p-4", + md: "p-6", + lg: "p-8", + }, + }, + defaultVariants: { + variant: "secondary", + padding: "md", + }, + } +) + +export interface CardProps + extends React.HTMLAttributes, + VariantProps {} + +const Card = React.forwardRef( + ({ className, variant, padding, ...props }, ref) => ( +
+ ) +) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, cardVariants } diff --git a/client/src/components/ui-new/dialog.tsx b/client/src/components/ui-new/dialog.tsx new file mode 100644 index 0000000..15952df --- /dev/null +++ b/client/src/components/ui-new/dialog.tsx @@ -0,0 +1,223 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "./button" + +/** + * Dialog Component + * Modal overlay with backdrop blur, focus trap, and keyboard support + */ +const dialogVariants = cva( + "relative z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]", + { + variants: { + size: { + sm: "max-w-sm", + md: "max-w-md", + lg: "max-w-lg", + xl: "max-w-xl", + "2xl": "max-w-2xl", + full: "max-w-full", + }, + }, + defaultVariants: { + size: "md", + }, + } +) + +export interface DialogProps { + open: boolean + onClose: () => void + children: React.ReactNode + size?: VariantProps["size"] + className?: string +} + +const Dialog = React.forwardRef( + ({ open, onClose, children, size, className }, forwardedRef) => { + const dialogRef = React.useRef(null) + + React.useImperativeHandle(forwardedRef, () => dialogRef.current!) + const [mounted, setMounted] = React.useState(false) + + React.useEffect(() => { + setMounted(true) + return () => setMounted(false) + }, []) + + React.useEffect(() => { + if (open) { + document.body.style.overflow = "hidden" + const firstFocusable = dialogRef.current?.querySelector( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) as HTMLElement | null + firstFocusable?.focus() + } else { + document.body.style.overflow = "" + } + return () => { + document.body.style.overflow = "" + } + }, [open]) + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!open) return + if (e.key === "Escape") { + onClose() + } + if (e.key === "Tab" && dialogRef.current) { + const focusableElements = dialogRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) + const firstElement = focusableElements[0] as HTMLElement + const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement + if (e.shiftKey && document.activeElement === firstElement) { + e.preventDefault() + lastElement.focus() + } else if (!e.shiftKey && document.activeElement === lastElement) { + e.preventDefault() + firstElement.focus() + } + } + } + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [open, onClose]) + + if (!mounted || !open) return null + + return ( +
+ + ) + } +) +Dialog.displayName = "Dialog" + +const DialogHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +DialogHeader.displayName = "DialogHeader" + +const DialogTitle = React.forwardRef< + HTMLHeadingElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +DialogTitle.displayName = "DialogTitle" + +const DialogDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +DialogDescription.displayName = "DialogDescription" + +const DialogContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +DialogContent.displayName = "DialogContent" + +const DialogFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +DialogFooter.displayName = "DialogFooter" + +const DialogCloseButton = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes & { onClose?: () => void } +>(({ onClose, className, ...props }, ref) => ( + +)) +DialogCloseButton.displayName = "DialogCloseButton" + +export { + Dialog, + DialogHeader, + DialogTitle, + DialogDescription, + DialogContent, + DialogFooter, + DialogCloseButton, + dialogVariants, +} diff --git a/client/src/components/ui-new/index.ts b/client/src/components/ui-new/index.ts new file mode 100644 index 0000000..7c491f6 --- /dev/null +++ b/client/src/components/ui-new/index.ts @@ -0,0 +1,47 @@ +/** + * UI Component Library - New Design System + * CreditNexus Professional Fintech Components + * + * @module ui-new + * @version 1.0.0 + */ + +// Core Components +export { Button, buttonVariants, type ButtonProps } from "./button" +export { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, + cardVariants, + type CardProps +} from "./card" +export { Input, Textarea, inputVariants, type InputProps, type TextareaProps } from "./input" + +// New Components +export { Badge, badgeVariants, type BadgeProps } from "./badge" +export { Select, selectVariants, type SelectProps, type SelectOption } from "./select" +export { + Dialog, + DialogHeader, + DialogTitle, + DialogDescription, + DialogContent, + DialogFooter, + DialogCloseButton, + dialogVariants, + type DialogProps, +} from "./dialog" + +// Theme Provider +export { + ThemeProvider, + useTheme, + ThemeToggle, + ThemeSelector, +} from "./theme-provider" + +// Utility +export { cn } from "./button" diff --git a/client/src/components/ui-new/input.tsx b/client/src/components/ui-new/input.tsx new file mode 100644 index 0000000..21d1aab --- /dev/null +++ b/client/src/components/ui-new/input.tsx @@ -0,0 +1,196 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "./button" + +/** + * Input Component Variants + * Text, Email, Password, and other input types + */ +const inputVariants = cva( + "flex w-full rounded-lg border bg-transparent px-4 py-3 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", + { + variants: { + variant: { + default: + "border-slate-600 bg-slate-800/50 text-slate-100 focus-visible:border-[#3B82F6] focus-visible:ring-[#3B82F6]", + ghost: + "border-transparent bg-slate-800/30 text-slate-100 focus-visible:bg-slate-800/50 focus-visible:ring-slate-400", + error: + "border-red-500 bg-red-500/10 text-slate-100 focus-visible:border-red-500 focus-visible:ring-red-500", + success: + "border-emerald-500 bg-emerald-500/10 text-slate-100 focus-visible:border-emerald-500 focus-visible:ring-emerald-500", + }, + size: { + sm: "h-8 px-3 text-sm", + md: "h-12 px-4 text-base", + lg: "h-14 px-4 text-lg", + }, + }, + defaultVariants: { + variant: "default", + size: "md", + }, + } +) + +export interface InputProps + extends Omit, "size">, + VariantProps { + error?: string + label?: string + helperText?: string +} + +const Input = React.forwardRef( + ({ className, variant, size, error, label, helperText, id, ...props }, ref) => { + const inputId = id || React.useId() + const hasError = !!error || variant === "error" + + return ( +
+ {label && ( + + )} + + {error && ( + + )} + {helperText && !error && ( +

+ {helperText} +

+ )} +
+ ) + } +) +Input.displayName = "Input" + +/** + * Textarea Component + */ +export interface TextareaProps + extends React.TextareaHTMLAttributes { + error?: string + label?: string + helperText?: string +} + +const Textarea = React.forwardRef( + ({ className, error, label, helperText, id, ...props }, ref) => { + const textareaId = id || React.useId() + const hasError = !!error + + return ( +
+ {label && ( + + )} +