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:
beabigegg
2025-11-13 08:55:01 +08:00
parent 9cf36d8e21
commit 57cf91271c
12 changed files with 2134 additions and 419 deletions

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }