diff --git a/reactrebuild0825/src/components/ui/Alert.tsx b/reactrebuild0825/src/components/ui/Alert.tsx new file mode 100644 index 0000000..6d402c6 --- /dev/null +++ b/reactrebuild0825/src/components/ui/Alert.tsx @@ -0,0 +1,150 @@ +import React, { forwardRef, useState } from 'react'; + +import { clsx } from 'clsx'; + +export interface AlertProps extends React.HTMLAttributes { + variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral'; + title?: string; + dismissible?: boolean; + onDismiss?: () => void; + icon?: React.ReactNode; + actions?: React.ReactNode; + children: React.ReactNode; +} + +const Alert = forwardRef( + ({ + variant = 'info', + title, + dismissible = false, + onDismiss, + icon, + actions, + children, + className, + ...props + }, ref) => { + const [isVisible, setIsVisible] = useState(true); + + const handleDismiss = () => { + setIsVisible(false); + onDismiss?.(); + }; + + if (!isVisible) {return null;} + + // Default icons for each variant + const defaultIcons = { + success: ( + + + + ), + warning: ( + + + + ), + error: ( + + + + ), + info: ( + + + + ), + neutral: ( + + + + ), + }; + + const alertStyles = clsx( + // Base styles + 'relative p-lg rounded-lg border backdrop-blur-md', + 'transition-all duration-200 ease-in-out', + + // Variant styles + { + 'bg-success-bg text-success-text border-success-border': variant === 'success', + 'bg-warning-bg text-warning-text border-warning-border': variant === 'warning', + 'bg-error-bg text-error-text border-error-border': variant === 'error', + 'bg-info-bg text-info-text border-info-border': variant === 'info', + 'bg-glass-bg text-text-primary border-glass-border': variant === 'neutral', + }, + + className + ); + + const iconColor = { + success: 'text-success-accent', + warning: 'text-warning-accent', + error: 'text-error-accent', + info: 'text-info-accent', + neutral: 'text-text-muted', + }; + + const displayIcon = icon ?? defaultIcons[variant]; + + return ( +
+
+ {displayIcon && ( +
+ {displayIcon} +
+ )} + +
+ {title && ( +

+ {title} +

+ )} + +
+ {children} +
+ + {actions && ( +
+ {actions} +
+ )} +
+ + {dismissible && ( +
+ +
+ )} +
+
+ ); + } +); + +Alert.displayName = 'Alert'; + +export { Alert }; \ No newline at end of file diff --git a/reactrebuild0825/src/components/ui/Badge.tsx b/reactrebuild0825/src/components/ui/Badge.tsx new file mode 100644 index 0000000..e0dc066 --- /dev/null +++ b/reactrebuild0825/src/components/ui/Badge.tsx @@ -0,0 +1,144 @@ +import React, { forwardRef } from 'react'; + +import { clsx } from 'clsx'; + +export interface BadgeProps extends React.HTMLAttributes { + variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'gold'; + size?: 'sm' | 'md' | 'lg'; + dot?: boolean; + removable?: boolean; + onRemove?: () => void; + children: React.ReactNode; +} + +const Badge = forwardRef( + ({ + variant = 'neutral', + size = 'md', + dot = false, + removable = false, + onRemove, + children, + className, + ...props + }, ref) => { + const baseStyles = clsx( + // Base styles + 'inline-flex items-center gap-xs font-medium backdrop-blur-md border', + 'transition-all duration-200 ease-in-out', + + // Size variants + { + 'px-sm py-xs text-xs rounded-md h-5': size === 'sm', + 'px-md py-xs text-sm rounded-md h-6': size === 'md', + 'px-lg py-sm text-base rounded-lg h-8': size === 'lg', + }, + + // Variant styles + { + // Success + 'bg-success-bg text-success-text border-success-border': variant === 'success', + + // Warning + 'bg-warning-bg text-warning-text border-warning-border': variant === 'warning', + + // Error + 'bg-error-bg text-error-text border-error-border': variant === 'error', + + // Info + 'bg-info-bg text-info-text border-info-border': variant === 'info', + + // Neutral + 'bg-glass-bg text-text-secondary border-glass-border': variant === 'neutral', + + // Gold + 'bg-gold-500/10 text-gold-text border-gold-500/30': variant === 'gold', + }, + + // Dot variant adjustments + { + 'pl-sm': dot && size === 'sm', + 'pl-md': dot && size === 'md', + 'pl-lg': dot && size === 'lg', + }, + + className + ); + + const dotStyles = clsx( + 'rounded-full', + { + 'w-1.5 h-1.5': size === 'sm', + 'w-2 h-2': size === 'md', + 'w-2.5 h-2.5': size === 'lg', + }, + { + 'bg-success-accent': variant === 'success', + 'bg-warning-accent': variant === 'warning', + 'bg-error-accent': variant === 'error', + 'bg-info-accent': variant === 'info', + 'bg-text-muted': variant === 'neutral', + 'bg-gold-500': variant === 'gold', + } + ); + + const removeButtonStyles = clsx( + 'ml-xs rounded-full transition-colors duration-150 focus:outline-none', + 'hover:bg-black/10 focus:bg-black/10', + { + 'w-3 h-3': size === 'sm', + 'w-4 h-4': size === 'md', + 'w-5 h-5': size === 'lg', + } + ); + + return ( + + {dot && + ); + } +); + +Badge.displayName = 'Badge'; + +export { Badge }; \ No newline at end of file diff --git a/reactrebuild0825/src/components/ui/Button.tsx b/reactrebuild0825/src/components/ui/Button.tsx new file mode 100644 index 0000000..052448a --- /dev/null +++ b/reactrebuild0825/src/components/ui/Button.tsx @@ -0,0 +1,87 @@ +import React, { forwardRef } from 'react'; + +import { clsx } from 'clsx'; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'gold' | 'ghost' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + loading?: boolean; + iconLeft?: React.ReactNode; + iconRight?: React.ReactNode; + children: React.ReactNode; +} + +const Button = forwardRef( + ({ + variant = 'primary', + size = 'md', + loading = false, + iconLeft, + iconRight, + children, + className, + disabled, + ...props + }, ref) => { + const baseStyles = clsx( + // Base glass styling + 'relative inline-flex items-center justify-center gap-sm font-medium', + 'transition-all duration-200 ease-in-out', + 'border border-glass-border backdrop-blur-md', + 'focus:outline-none focus:ring-2 focus:ring-gold-500 focus:ring-offset-2 focus:ring-offset-background-primary', + 'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none', + + // Size variants + { + 'px-md py-xs text-sm h-8 rounded-md': size === 'sm', + 'px-lg py-sm text-base h-10 rounded-lg': size === 'md', + 'px-xl py-md text-lg h-12 rounded-xl': size === 'lg', + }, + + // Variant styles + { + // Primary - Blue gradient with glass effect + 'bg-glass-bg text-primary-text border-primary-500/30 shadow-glass hover:bg-primary-500/20 hover:border-primary-400/50 hover:shadow-glow-sm active:scale-95': variant === 'primary', + + // Secondary - Purple gradient with glass effect + 'bg-glass-bg text-secondary-text border-secondary-500/30 shadow-glass hover:bg-secondary-500/20 hover:border-secondary-400/50 hover:shadow-glow-sm active:scale-95': variant === 'secondary', + + // Gold - Premium gold styling + 'bg-glass-bg text-gold-text border-gold-500/30 shadow-glass hover:bg-gold-500/20 hover:border-gold-400/50 hover:shadow-glow active:scale-95': variant === 'gold', + + // Ghost - Minimal glass styling + 'bg-transparent text-text-primary border-border-default hover:bg-glass-bg hover:border-glass-border hover:shadow-glass-sm active:scale-95': variant === 'ghost', + + // Danger - Error styling with glass effect + 'bg-glass-bg text-error-text border-error-accent/30 shadow-glass hover:bg-error-accent/20 hover:border-error-accent/50 hover:shadow-glow-sm active:scale-95': variant === 'danger', + } + ); + + const isDisabled = disabled ?? loading; + + return ( + + ); + } +); + +Button.displayName = 'Button'; + +export { Button }; \ No newline at end of file diff --git a/reactrebuild0825/src/components/ui/Card.tsx b/reactrebuild0825/src/components/ui/Card.tsx new file mode 100644 index 0000000..e6a2f41 --- /dev/null +++ b/reactrebuild0825/src/components/ui/Card.tsx @@ -0,0 +1,167 @@ +import React, { forwardRef } from 'react'; + +import { clsx } from 'clsx'; + +export interface CardProps { + variant?: 'default' | 'elevated' | 'outline'; + padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl'; + clickable?: boolean; + elevation?: 'sm' | 'md' | 'lg' | 'xl'; + children: React.ReactNode; + onClick?: () => void; + className?: string; + id?: string; + 'data-testid'?: string; +} + +export interface CardHeaderProps extends React.HTMLAttributes { + children: React.ReactNode; +} + +export interface CardBodyProps extends React.HTMLAttributes { + children: React.ReactNode; +} + +export interface CardFooterProps extends React.HTMLAttributes { + children: React.ReactNode; +} + +const Card = forwardRef( + ({ + variant = 'default', + padding = 'md', + clickable = false, + elevation = 'md', + children, + className, + onClick, + id, + 'data-testid': dataTestId + }, ref) => { + const cardStyles = clsx( + // Base styles + 'relative bg-glass-bg backdrop-blur-md rounded-lg border transition-all duration-200 ease-in-out', + + // Variant styles + { + 'border-glass-border': variant === 'default', + 'border-glass-border shadow-glass-lg': variant === 'elevated', + 'border-border-default bg-transparent': variant === 'outline', + }, + + // Elevation styles + { + 'shadow-glass-sm': elevation === 'sm' && variant !== 'outline', + 'shadow-glass': elevation === 'md' && variant !== 'outline', + 'shadow-glass-lg': elevation === 'lg' && variant !== 'outline', + 'shadow-glass-xl': elevation === 'xl' && variant !== 'outline', + }, + + // Padding styles + { + 'p-0': padding === 'none', + 'p-sm': padding === 'sm', + 'p-lg': padding === 'md', + 'p-xl': padding === 'lg', + 'p-2xl': padding === 'xl', + }, + + // Clickable styles + { + 'cursor-pointer hover:shadow-glass-lg hover:border-gold-500/30 hover:bg-glass-bg/80 active:scale-[0.98]': clickable && variant !== 'outline', + 'cursor-pointer hover:border-gold-500/50 hover:bg-glass-bg active:scale-[0.98]': clickable && variant === 'outline', + }, + + className + ); + + if (clickable) { + return ( + + ); + } + + return ( +
) => { + if ((e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onClick(); + } + } : undefined} + role={onClick ? 'button' : undefined} + tabIndex={onClick ? 0 : undefined} + > + {children} +
+ ); + } +); + +const CardHeader = forwardRef( + ({ children, className, ...props }, ref) => ( +
+ {children} +
+ ) +); + +const CardBody = forwardRef( + ({ children, className, ...props }, ref) => ( +
+ {children} +
+ ) +); + +const CardFooter = forwardRef( + ({ children, className, ...props }, ref) => ( +
+ {children} +
+ ) +); + +Card.displayName = 'Card'; +CardHeader.displayName = 'CardHeader'; +CardBody.displayName = 'CardBody'; +CardFooter.displayName = 'CardFooter'; + +export { Card, CardHeader, CardBody, CardFooter }; \ No newline at end of file diff --git a/reactrebuild0825/src/components/ui/Input.tsx b/reactrebuild0825/src/components/ui/Input.tsx new file mode 100644 index 0000000..d4d1126 --- /dev/null +++ b/reactrebuild0825/src/components/ui/Input.tsx @@ -0,0 +1,124 @@ +import React, { forwardRef } from 'react'; + +import { clsx } from 'clsx'; + +export interface InputProps extends React.InputHTMLAttributes { + label?: string; + helperText?: string; + error?: string; + iconLeft?: React.ReactNode; + iconRight?: React.ReactNode; + variant?: 'default' | 'ghost'; +} + +const Input = forwardRef( + ({ + label, + helperText, + error, + iconLeft, + iconRight, + variant = 'default', + className, + id, + ...props + }, ref) => { + const inputId = id ?? `input-${Math.random().toString(36).substr(2, 9)}`; + const helperTextId = `${inputId}-helper`; + const errorId = `${inputId}-error`; + + const inputStyles = clsx( + // Base glass styling + 'w-full px-lg py-sm text-base text-text-primary placeholder-text-muted', + 'bg-glass-bg border border-glass-border backdrop-blur-md rounded-lg', + 'transition-all duration-200 ease-in-out', + 'focus:outline-none focus:ring-2 focus:ring-gold-500 focus:ring-offset-2 focus:ring-offset-background-primary', + 'focus:border-gold-500/50 focus:bg-glass-bg', + 'disabled:opacity-50 disabled:cursor-not-allowed', + + // Variant styles + { + 'shadow-glass hover:shadow-glass-lg': variant === 'default', + 'bg-transparent border-border-default hover:bg-glass-bg hover:border-glass-border': variant === 'ghost', + }, + + // Icon padding adjustments + { + 'pl-10': iconLeft, + 'pr-10': iconRight, + }, + + // Error states + { + 'border-error-accent/50 focus:ring-error-accent focus:border-error-accent/70': error, + 'border-glass-border': !error, + }, + + className + ); + + return ( +
+ {label && ( + + )} + +
+ {iconLeft && ( +
+ {iconLeft} +
+ )} + + + + {iconRight && ( +
+ {iconRight} +
+ )} +
+ + {(helperText ?? error) && ( +
+ {error ? ( + + ) : helperText ? ( +

+ {helperText} +

+ ) : null} +
+ )} +
+ ); + } +); + +Input.displayName = 'Input'; + +export { Input }; \ No newline at end of file diff --git a/reactrebuild0825/src/components/ui/Select.tsx b/reactrebuild0825/src/components/ui/Select.tsx new file mode 100644 index 0000000..2c3f069 --- /dev/null +++ b/reactrebuild0825/src/components/ui/Select.tsx @@ -0,0 +1,292 @@ +import { forwardRef, useState, useRef, useEffect } from 'react'; + +import { clsx } from 'clsx'; + +export interface SelectOption { + value: string; + label: string; + disabled?: boolean; +} + +export interface SelectProps { + options: SelectOption[]; + value?: string; + placeholder?: string; + label?: string; + helperText?: string; + error?: string; + disabled?: boolean; + searchable?: boolean; + multiple?: boolean; + onChange?: (value: string | string[]) => void; + onSearch?: (query: string) => void; + className?: string; + id?: string; +} + +const Select = forwardRef( + ({ + options, + value, + placeholder = 'Select an option...', + label, + helperText, + error, + disabled = false, + searchable = false, + multiple = false, + onChange, + onSearch, + className, + id, + ...props + }, ref) => { + const [isOpen, setIsOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedValues, setSelectedValues] = useState( + multiple ? (Array.isArray(value) ? value : value ? [value] : []) : value ? [value] : [] + ); + + const selectRef = useRef(null); + const searchInputRef = useRef(null); + + const selectId = id ?? `select-${Math.random().toString(36).substr(2, 9)}`; + const helperTextId = `${selectId}-helper`; + const errorId = `${selectId}-error`; + + // Filter options based on search query + const filteredOptions = searchable && searchQuery + ? options.filter(option => + option.label.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : options; + + // Get display value + const getDisplayValue = () => { + if (selectedValues.length === 0) {return placeholder;} + + if (multiple) { + return selectedValues.length === 1 + ? options.find(opt => opt.value === selectedValues[0])?.label + : `${selectedValues.length} selected`; + } + + return options.find(opt => opt.value === selectedValues[0])?.label ?? placeholder; + }; + + // Handle option selection + const handleOptionSelect = (optionValue: string) => { + let newSelectedValues: string[]; + + if (multiple) { + newSelectedValues = selectedValues.includes(optionValue) + ? selectedValues.filter(v => v !== optionValue) + : [...selectedValues, optionValue]; + } else { + newSelectedValues = [optionValue]; + setIsOpen(false); + } + + setSelectedValues(newSelectedValues); + onChange?.(multiple ? newSelectedValues : newSelectedValues[0] ?? ''); + }; + + // Handle search + const handleSearch = (query: string) => { + setSearchQuery(query); + onSearch?.(query); + }; + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (selectRef.current && !selectRef.current.contains(event.target as Node)) { + setIsOpen(false); + setSearchQuery(''); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Focus search input when dropdown opens + useEffect(() => { + if (isOpen && searchable && searchInputRef.current) { + searchInputRef.current.focus(); + } + }, [isOpen, searchable]); + + const triggerStyles = clsx( + // Base glass styling + 'w-full px-lg py-sm text-base text-left', + 'bg-glass-bg border border-glass-border backdrop-blur-md rounded-lg', + 'transition-all duration-200 ease-in-out cursor-pointer', + 'focus:outline-none focus:ring-2 focus:ring-gold-500 focus:ring-offset-2 focus:ring-offset-background-primary', + 'focus:border-gold-500/50 shadow-glass hover:shadow-glass-lg', + 'disabled:opacity-50 disabled:cursor-not-allowed', + + // Error states + { + 'border-error-accent/50 focus:ring-error-accent focus:border-error-accent/70': error, + 'border-glass-border': !error, + }, + + // Open state + { + 'border-gold-500/50 ring-2 ring-gold-500 ring-offset-2 ring-offset-background-primary': isOpen && !error, + } + ); + + const dropdownStyles = clsx( + 'absolute z-50 w-full mt-xs', + 'bg-glass-bg border border-glass-border backdrop-blur-md rounded-lg shadow-glass-lg', + 'max-h-60 overflow-auto' + ); + + const optionStyles = (option: SelectOption, isSelected: boolean) => clsx( + 'w-full px-lg py-sm text-left text-base transition-colors duration-150', + 'hover:bg-glass-bg focus:bg-glass-bg focus:outline-none', + { + 'text-text-primary': !option.disabled, + 'text-text-disabled cursor-not-allowed': option.disabled, + 'bg-gold-500/20 text-gold-text': isSelected && !option.disabled, + } + ); + + return ( +
+ {label && ( + + )} + +
+
!disabled && setIsOpen(!isOpen)} + onKeyDown={(e) => { + if (disabled) {return;} + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setIsOpen(!isOpen); + } + if (e.key === 'Escape') { + setIsOpen(false); + } + }} + > +
+ + {getDisplayValue()} + + + + +
+
+ + {isOpen && ( +
+ {searchable && ( +
+ handleSearch(e.target.value)} + className="w-full px-sm py-xs text-sm bg-transparent border-none focus:outline-none text-text-primary placeholder-text-muted" + /> +
+ )} + +
+ {filteredOptions.length === 0 ? ( +
+ No options found +
+ ) : ( + filteredOptions.map((option) => { + const isSelected = selectedValues.includes(option.value); + return ( + + ); + }) + )} +
+
+ )} +
+ + {(helperText ?? error) && ( +
+ {error ? ( + + ) : helperText ? ( +

+ {helperText} +

+ ) : null} +
+ )} +
+ ); + } +); + +Select.displayName = 'Select'; + +export { Select }; \ No newline at end of file diff --git a/reactrebuild0825/src/components/ui/index.ts b/reactrebuild0825/src/components/ui/index.ts new file mode 100644 index 0000000..a5bd132 --- /dev/null +++ b/reactrebuild0825/src/components/ui/index.ts @@ -0,0 +1,7 @@ +// UI Component Library Exports +export { Button, type ButtonProps } from './Button'; +export { Input, type InputProps } from './Input'; +export { Select, type SelectProps, type SelectOption } from './Select'; +export { Card, CardHeader, CardBody, CardFooter, type CardProps, type CardHeaderProps, type CardBodyProps, type CardFooterProps } from './Card'; +export { Badge, type BadgeProps } from './Badge'; +export { Alert, type AlertProps } from './Alert'; \ No newline at end of file diff --git a/reactrebuild0825/src/contexts/ThemeContext.tsx b/reactrebuild0825/src/contexts/ThemeContext.tsx index 5f4a4ad..0096f41 100644 --- a/reactrebuild0825/src/contexts/ThemeContext.tsx +++ b/reactrebuild0825/src/contexts/ThemeContext.tsx @@ -63,6 +63,7 @@ export function ThemeProvider({ children, defaultTheme = 'dark' }: ThemeProvider mediaQuery.addEventListener('change', handleChange); return () => mediaQuery.removeEventListener('change', handleChange); } + return undefined; }, []); const value: ThemeContextType = { diff --git a/reactrebuild0825/src/utils/contrast.ts b/reactrebuild0825/src/utils/contrast.ts index 7a29bec..deba2ea 100644 --- a/reactrebuild0825/src/utils/contrast.ts +++ b/reactrebuild0825/src/utils/contrast.ts @@ -7,9 +7,9 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), + r: parseInt(result[1]!, 16), + g: parseInt(result[2]!, 16), + b: parseInt(result[3]!, 16), } : null; } @@ -17,16 +17,16 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null { // Convert RGBA string to RGB values function rgbaToRgb(rgba: string): { r: number; g: number; b: number; a: number } | null { const match = rgba.match(/rgba?\(([^)]+)\)/); - if (!match) { + if (!match?.[1]) { return null; } const values = match[1].split(',').map(v => parseFloat(v.trim())); return { - r: values[0], - g: values[1], - b: values[2], - a: values[3] || 1, + r: values[0] ?? 0, + g: values[1] ?? 0, + b: values[2] ?? 0, + a: values[3] ?? 1, }; } @@ -36,7 +36,7 @@ function getLuminance(r: number, g: number, b: number): number { c = c / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }); - return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; + return 0.2126 * (rs ?? 0) + 0.7152 * (gs ?? 0) + 0.0722 * (bs ?? 0); } // Calculate contrast ratio between two colors