feat(web): заготовка веб-клиента на Vite + React 19 + TypeScript strict
- стек по §5.2: React 19, React Router, TanStack Query, Zustand, Tailwind CSS 4, i18next (ru/en), Vitest + Testing Library - дизайн-токены §3 docs/client-web.md (тёмная тема по умолчанию, светлая, accent) - типизированный API-клиент §8.1: единый формат ошибок, ApiError с кодом - App: экран состояния фундамента с readz/meta, локализация ошибок по code - тесты: клиент API, форматирование, рендер App (успех/ошибка кода/сеть) - eslint + prettier + tsc strict; initial JS 97.6 KB gzip
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { formatBytes } from '@/lib/format';
|
||||
|
||||
interface StatRowProps {
|
||||
label: string;
|
||||
value: ReactNode;
|
||||
}
|
||||
|
||||
export function StatRow({ label, value }: StatRowProps) {
|
||||
return (
|
||||
<div className="flex items-baseline justify-between gap-4 border-b border-border/40 py-2 last:border-b-0">
|
||||
<dt className="text-fg-muted">{label}</dt>
|
||||
<dd className="font-medium tabular-nums">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatBytes({ bytes, locale }: { bytes: number; locale: string }) {
|
||||
return <span title={`${bytes} B`}>{formatBytes(bytes, locale)}</span>;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ButtonHTMLAttributes, HTMLAttributes } from 'react';
|
||||
|
||||
type Variant = 'primary' | 'ghost';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variant;
|
||||
}
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
primary:
|
||||
'bg-accent text-accent-fg hover:opacity-90 border border-transparent shadow-[var(--shadow-1)]',
|
||||
ghost: 'bg-surface-2 text-fg hover:bg-surface-3 border border-border/60',
|
||||
};
|
||||
|
||||
export function Button({ variant = 'primary', className = '', ...rest }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] px-3 py-1.5 text-sm font-medium transition-colors duration-[var(--duration-fast)] disabled:cursor-not-allowed disabled:opacity-50 ${variants[variant]} ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Card({ className = '', ...rest }: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-[var(--radius-lg)] border border-border/60 bg-surface-1 p-5 shadow-[var(--shadow-2)] ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Badge({ className = '', ...rest }: HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full border border-border/60 bg-surface-2 px-2 py-0.5 text-xs text-fg-muted ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user