refactor: complete application rewrite with modern UI
This commit is contained in:
42
src/components/ui/Badge.tsx
Normal file
42
src/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
export type BadgeVariant = 'default' | 'success' | 'warning' | 'danger' | 'info';
|
||||
|
||||
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
variant?: BadgeVariant;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
const variantStyles: Record<BadgeVariant, string> = {
|
||||
default: 'bg-slate-100 text-slate-700',
|
||||
success: 'bg-green-100 text-green-800',
|
||||
warning: 'bg-yellow-100 text-yellow-800',
|
||||
danger: 'bg-red-100 text-red-800',
|
||||
info: 'bg-blue-100 text-blue-800',
|
||||
};
|
||||
|
||||
const sizeStyles: Record<'sm' | 'md', string> = {
|
||||
sm: 'px-1.5 py-0.5 text-xs',
|
||||
md: 'px-2 py-1 text-sm',
|
||||
};
|
||||
|
||||
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
({ className = '', variant = 'default', size = 'sm', children, ...props }, ref) => {
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={`
|
||||
inline-flex items-center font-medium rounded-full
|
||||
${variantStyles[variant]}
|
||||
${sizeStyles[size]}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
71
src/components/ui/Button.tsx
Normal file
71
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
export type ButtonVariant = 'primary' | 'success' | 'danger' | 'secondary' | 'ghost';
|
||||
export type ButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
icon?: LucideIcon;
|
||||
iconPosition?: 'left' | 'right';
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const variantStyles: Record<ButtonVariant, string> = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 shadow-md hover:shadow-lg',
|
||||
success: 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500 shadow-md hover:shadow-lg',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 shadow-md hover:shadow-lg',
|
||||
secondary: 'bg-slate-200 text-slate-700 hover:bg-slate-300 focus:ring-slate-400',
|
||||
ghost: 'bg-transparent text-slate-600 hover:bg-slate-100 focus:ring-slate-400',
|
||||
};
|
||||
|
||||
const sizeStyles: Record<ButtonSize, string> = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-base',
|
||||
lg: 'px-6 py-3 text-lg',
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
className = '',
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
icon: Icon,
|
||||
iconPosition = 'left',
|
||||
isLoading = false,
|
||||
disabled,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const baseStyles = `
|
||||
inline-flex items-center justify-center gap-2
|
||||
font-medium rounded-lg
|
||||
transition-all duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${variantStyles[variant]}
|
||||
${sizeStyles[size]}
|
||||
${className}
|
||||
`;
|
||||
|
||||
return (
|
||||
<button ref={ref} className={baseStyles} disabled={disabled || isLoading} {...props}>
|
||||
{isLoading ? (
|
||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
) : Icon ? (
|
||||
<Icon className="w-4 h-4" />
|
||||
) : null}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
85
src/components/ui/Card.tsx
Normal file
85
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'bordered' | 'elevated';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const variantStyles: Record<string, string> = {
|
||||
default: 'bg-white',
|
||||
bordered: 'bg-white border-2 border-slate-200',
|
||||
elevated: 'bg-white shadow-lg',
|
||||
};
|
||||
|
||||
const paddingStyles: Record<string, string> = {
|
||||
none: '',
|
||||
sm: 'p-3',
|
||||
md: 'p-4',
|
||||
lg: 'p-6',
|
||||
};
|
||||
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className = '', variant = 'default', padding = 'md', children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`rounded-xl overflow-hidden transition-all duration-300 ${variantStyles[variant]} ${paddingStyles[padding]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
||||
({ className = '', children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`px-4 py-3 border-b border-slate-200 bg-slate-50 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
interface CardBodyProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
|
||||
({ className = '', children, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={`p-4 ${className}`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardBody.displayName = 'CardBody';
|
||||
|
||||
interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
|
||||
({ className = '', children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`px-4 py-3 border-t border-slate-200 bg-slate-50 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
49
src/components/ui/CodeBlock.tsx
Normal file
49
src/components/ui/CodeBlock.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
import Highlight from 'react-highlight';
|
||||
import 'highlight.js/styles/atom-one-light.css';
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: 'json' | 'xml' | 'javascript' | 'typescript' | 'css' | 'html' | 'plaintext';
|
||||
showLineNumbers?: boolean;
|
||||
maxHeight?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({
|
||||
code,
|
||||
language = 'plaintext',
|
||||
showLineNumbers = false,
|
||||
maxHeight = 'none',
|
||||
className = '',
|
||||
}: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative group ${className}`}>
|
||||
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="px-2 py-1 text-xs bg-slate-800 text-white rounded hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-lg overflow-hidden border border-slate-200"
|
||||
style={{ maxHeight, overflow: maxHeight !== 'none' ? 'auto' : 'visible' }}
|
||||
>
|
||||
<Highlight className={`language-${language} text-sm ${showLineNumbers ? 'line-numbers' : ''}`}>
|
||||
{code || '// Empty'}
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
src/components/ui/Input.tsx
Normal file
68
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { InputHTMLAttributes, forwardRef, useState } from 'react';
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
hint?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, hint, icon, className = '', id, ...props }, ref) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
const baseStyles = `
|
||||
w-full px-3 py-2
|
||||
border rounded-lg
|
||||
transition-all duration-200
|
||||
focus:outline-none focus:ring-2 focus:border-transparent
|
||||
disabled:bg-slate-100 disabled:cursor-not-allowed
|
||||
${error
|
||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-slate-300 focus:ring-blue-500 focus:border-blue-500'
|
||||
}
|
||||
${isFocused ? 'ring-2 ring-blue-500 border-transparent bg-blue-50' : 'bg-white'}
|
||||
${icon ? 'pl-10' : ''}
|
||||
${className}
|
||||
`;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-slate-700 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
{icon && (
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={baseStyles}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mt-1 text-sm text-red-600">{error}</p>
|
||||
)}
|
||||
|
||||
{hint && !error && (
|
||||
<p className="mt-1 text-sm text-slate-500">{hint}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
55
src/components/ui/Select.tsx
Normal file
55
src/components/ui/Select.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { SelectHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
options: { value: string | number; label: string; disabled?: boolean }[];
|
||||
}
|
||||
|
||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ label, error, options, className = '', id, ...props }, ref) => {
|
||||
const selectId = id || label?.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
const baseStyles = `
|
||||
w-full px-3 py-2
|
||||
border rounded-lg
|
||||
bg-white cursor-pointer
|
||||
transition-all duration-200
|
||||
focus:outline-none focus:ring-2 focus:border-transparent
|
||||
disabled:bg-slate-100 disabled:cursor-not-allowed
|
||||
${error
|
||||
? 'border-red-300 focus:ring-red-500'
|
||||
: 'border-slate-300 focus:ring-blue-500'
|
||||
}
|
||||
${className}
|
||||
`;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label htmlFor={selectId} className="block text-sm font-medium text-slate-700 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<select ref={ref} id={selectId} className={baseStyles} {...props}>
|
||||
{options.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{error && (
|
||||
<p className="mt-1 text-sm text-red-600">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Select.displayName = 'Select';
|
||||
75
src/components/ui/Tabs.tsx
Normal file
75
src/components/ui/Tabs.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
export interface Tab {
|
||||
id: string;
|
||||
label: string;
|
||||
badge?: string | number;
|
||||
badgeVariant?: 'default' | 'success' | 'warning' | 'danger';
|
||||
}
|
||||
|
||||
export interface TabsProps {
|
||||
tabs: Tab[];
|
||||
activeTab: string;
|
||||
onChange: (tabId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Tabs({ tabs, activeTab, onChange, className = '', ...props }: TabsProps) {
|
||||
return (
|
||||
<div className={`border-b border-slate-200 ${className}`} {...props}>
|
||||
<nav className="-mb-px flex gap-2" aria-label="Tabs">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTab;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => onChange(tab.id)}
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-t-lg
|
||||
border-b-2 transition-all duration-200
|
||||
${isActive
|
||||
? 'border-blue-500 text-blue-600 bg-white'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 hover:bg-slate-50'
|
||||
}
|
||||
`}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.badge !== undefined && (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||||
${tab.badgeVariant === 'danger' || tab.badgeVariant === 'warning'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TabPanelProps extends HTMLAttributes<HTMLDivElement> {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export function TabPanel({ isActive, children, className = '', ...props }: TabPanelProps) {
|
||||
if (!isActive) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`animate-fade-in ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/components/ui/index.ts
Normal file
16
src/components/ui/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { Button } from './Button';
|
||||
export type { ButtonVariant, ButtonSize } from './Button';
|
||||
|
||||
export { Input } from './Input';
|
||||
|
||||
export { Card, CardHeader, CardBody, CardFooter } from './Card';
|
||||
|
||||
export { Select } from './Select';
|
||||
|
||||
export { Badge } from './Badge';
|
||||
export type { BadgeVariant } from './Badge';
|
||||
|
||||
export { Tabs, TabPanel } from './Tabs';
|
||||
export type { Tab } from './Tabs';
|
||||
|
||||
export { CodeBlock } from './CodeBlock';
|
||||
Reference in New Issue
Block a user