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:
2026-09-19 18:20:59 +03:00
parent 69aa98e18c
commit 642d0f720e
30 changed files with 5776 additions and 0 deletions
+21
View File
@@ -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>;
}
+41
View File
@@ -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}
/>
);
}