refactor: complete application rewrite with modern UI

This commit is contained in:
sokol
2026-02-19 22:55:26 +03:00
parent 271b530fa1
commit a6cc5a9827
26 changed files with 3036 additions and 794 deletions

View 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';

View 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';

View 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';

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

View 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';

View 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';

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

View 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';