feat: modernize frontend UI with Tailwind v4 and professional design system
BREAKING CHANGE: Migrated to Tailwind CSS v4 configuration system Key Changes: - Migrated from Tailwind v3 to v4 configuration system - Removed tailwind.config.js (incompatible with v4) - Updated index.css with @theme directive and oklch color space - Defined all custom animations directly in CSS using @keyframes - Redesigned LoginPage with modern, enterprise-grade UI: - Full-screen gradient background (blue → purple → pink) - Floating animated orbs with blur effects - Glass morphism white card with backdrop-blur - Gradient buttons with shadow effects - 7 custom animations: fade-in, slide-in-right, slide-in-left, scale-in, shimmer, pulse, float - Added shadcn/ui components: - alert.tsx, dialog.tsx, input.tsx, label.tsx, select.tsx, tabs.tsx - Updated dependencies: - Added class-variance-authority ^0.7.0 - Added react-markdown ^9.0.1 - Updated frontend documentation: - Comprehensive README.md with feature list, tech stack, project structure - Quick start guide and deployment instructions Technical Details: - Tailwind v4 uses @import "tailwindcss" instead of @tailwind directives - All theme customization now in @theme block with CSS variables - Color system migrated to oklch for better perceptual uniformity - Animation definitions moved from config to CSS @layer utilities 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
76
frontend/src/components/ui/alert.tsx
Normal file
76
frontend/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AlertCircle, CheckCircle2, Info, AlertTriangle } from 'lucide-react'
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:pl-7',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground border-border',
|
||||
info: 'bg-primary/10 text-primary border-primary/30',
|
||||
success: 'bg-success/10 text-success border-success/30',
|
||||
warning: 'bg-warning/10 text-warning border-warning/30',
|
||||
destructive: 'bg-destructive/10 text-destructive border-destructive/30',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const icons = {
|
||||
default: Info,
|
||||
info: Info,
|
||||
success: CheckCircle2,
|
||||
warning: AlertTriangle,
|
||||
destructive: AlertCircle,
|
||||
}
|
||||
|
||||
export interface AlertProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof alertVariants> {
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||
({ className, variant = 'default', showIcon = true, children, ...props }, ref) => {
|
||||
const Icon = icons[variant || 'default']
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && <Icon className="h-5 w-5" />}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Alert.displayName = 'Alert'
|
||||
|
||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
AlertTitle.displayName = 'AlertTitle'
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
|
||||
))
|
||||
AlertDescription.displayName = 'AlertDescription'
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
118
frontend/src/components/ui/dialog.tsx
Normal file
118
frontend/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
export interface DialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Dialog: React.FC<DialogProps> = ({ open, onClose, children }) => {
|
||||
React.useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
if (open) {
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
document.body.style.overflow = 'unset'
|
||||
}
|
||||
}, [open, onClose])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* Dialog */}
|
||||
<div className="relative z-50 w-full max-w-lg mx-4 animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-card rounded-xl border border-border shadow-xl',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
DialogContent.displayName = 'DialogContent'
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex items-center justify-between p-6 border-b border-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
|
||||
const DialogTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h2
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
DialogTitle.displayName = 'DialogTitle'
|
||||
|
||||
const DialogBody = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('p-6', className)} {...props} />
|
||||
)
|
||||
DialogBody.displayName = 'DialogBody'
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex items-center justify-end gap-3 p-6 border-t border-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
|
||||
const DialogClose = ({
|
||||
onClose,
|
||||
className,
|
||||
}: {
|
||||
onClose: () => void
|
||||
className?: string
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'p-2 rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-colors',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
)
|
||||
|
||||
export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooter, DialogClose }
|
||||
28
frontend/src/components/ui/input.tsx
Normal file
28
frontend/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm',
|
||||
'ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium',
|
||||
'placeholder:text-muted-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:border-primary',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'transition-colors',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
23
frontend/src/components/ui/label.tsx
Normal file
23
frontend/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
||||
|
||||
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-sm font-medium leading-none text-foreground',
|
||||
'peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Label.displayName = 'Label'
|
||||
|
||||
export { Label }
|
||||
38
frontend/src/components/ui/select.tsx
Normal file
38
frontend/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
|
||||
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
options: Array<{ value: string; label: string }>
|
||||
}
|
||||
|
||||
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ className, options, ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<select
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm',
|
||||
'ring-offset-background',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:border-primary',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'transition-colors appearance-none pr-10',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Select.displayName = 'Select'
|
||||
|
||||
export { Select }
|
||||
108
frontend/src/components/ui/tabs.tsx
Normal file
108
frontend/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface TabsProps {
|
||||
defaultValue?: string
|
||||
value?: string
|
||||
onValueChange?: (value: string) => void
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface TabsContextValue {
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
}
|
||||
|
||||
const TabsContext = React.createContext<TabsContextValue | undefined>(undefined)
|
||||
|
||||
const Tabs: React.FC<TabsProps> = ({
|
||||
defaultValue,
|
||||
value: controlledValue,
|
||||
onValueChange,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const [internalValue, setInternalValue] = React.useState(defaultValue || '')
|
||||
const value = controlledValue !== undefined ? controlledValue : internalValue
|
||||
const handleValueChange = onValueChange || setInternalValue
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
|
||||
<div className={className}>{children}</div>
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const TabsList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
TabsList.displayName = 'TabsList'
|
||||
|
||||
interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
value: string
|
||||
}
|
||||
|
||||
const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
|
||||
({ className, value, ...props }, ref) => {
|
||||
const context = React.useContext(TabsContext)
|
||||
if (!context) throw new Error('TabsTrigger must be used within Tabs')
|
||||
|
||||
const isSelected = context.value === value
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={() => context.onValueChange(value)}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium',
|
||||
'ring-offset-background transition-all',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-2',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
isSelected
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
TabsTrigger.displayName = 'TabsTrigger'
|
||||
|
||||
interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
value: string
|
||||
}
|
||||
|
||||
const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(
|
||||
({ className, value, ...props }, ref) => {
|
||||
const context = React.useContext(TabsContext)
|
||||
if (!context) throw new Error('TabsContent must be used within Tabs')
|
||||
|
||||
if (context.value !== value) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
TabsContent.displayName = 'TabsContent'
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
Reference in New Issue
Block a user