Files
voidea/webui/STYLE_GUIDE.md
T

6.8 KiB

VoIdea WebUI Style Guide

Tech Stack

Category Choice Version
Framework React 18.3.x (LTS)
Language TypeScript 5.5+ (strict mode)
Routing react-router-dom 6.26.x
Styling Tailwind CSS 3.4.x
State Zustand (new stores) + Context (existing) 5.x
Forms react-hook-form + zod latest
i18n react-i18next (future) / constants/strings.ts (now)
Testing Vitest + @testing-library/react latest
PWA vite-plugin-pwa 0.20.x
Build Vite 5.4.x

Project Structure

webui/src/
├── api/          # HTTP client + endpoint modules
├── auth/         # Zustand store (future), Context (migration target)
├── components/   # Shared UI (ErrorBoundary, Layout, VoiceChat, etc.)
├── constants/    # strings.ts (i18n-ready), enums
├── hooks/        # Custom hooks
├── pages/        # Route pages (one file per route)
├── stores/       # Zustand stores (auth, ideas, settings...)
├── types/        # Shared TypeScript types
├── utils/        # Pure utility functions
├── App.tsx       # Router setup
└── main.tsx      # Entry point

Coding Rules

General

  • any запрещён. Всегда явный тип.
  • Импорты: абсолютные через @/ alias. Относительные только для соседних файлов.
  • Файл — не более 300 строк. Сервисы/компоненты больше — разбить.
  • Prettier: 100 символов, двойные кавычки, точка с запятой.

State Management (Zustand)

// stores/auth.ts
import { create } from "zustand";

interface AuthState {
  user: User | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  token: null,
  login: async (email, password) => { /* ... */ },
  logout: () => { clearTokens(); set({ user: null, token: null }); },
}));
  • Один store — одна доменная область (auth, ideas, settings, voice).
  • Store НЕ содержит UI-логику (только состояние + действия).
  • Использовать useAuthStore() в компонентах напрямую (без Provider).
  • Context API — только для редких случаев (< 3 подписчиков, Legacy).

Forms (react-hook-form + zod)

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

type FormData = z.infer<typeof schema>;

const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
  resolver: zodResolver(schema),
});
  • Каждая сложная форма (3+ поля) — свой schema-файл в pages/<name>.schema.ts.
  • Простые формы (1–2 поля, логин) — schema внутри компонента.
  • Ошибки выводить <span className="text-red-500 text-sm">.
  • Кнопку сабмита дизейблить пока isSubmitting.

Accessibility (WCAG AA)

Обязательно:

  • aria-label на всех кнопках без текста (иконки: Menu, ThemeToggle, Logout, Close, VoiceChat mic)
  • htmlFor + id на всех <label>/<input> парах
  • role="log" + aria-live="polite" на VoiceChat
  • <SkipToContent /> — первая ссылка в <body>
  • Focus trap в модалках (первый элемент при открытии, возврат при закрытии)
  • Цветовой контраст ≥ 4.5:1 (проверять text-gray-500 на bg-gray-800)

Проверка: eslint-plugin-jsx-a11y (strict ruleset, errors = fail CI).

Error Boundaries

  • Глобальный <ErrorBoundary> вокруг <Layout> в App.tsx.
  • Опционально: per-page ErrorBoundary для VoiceChat, AdminPage (высокая сложность).
  • Fallback: <ServerErrorPage /> с кнопкой «На главную».
<ErrorBoundary fallback={<ServerErrorPage />}>
  <Layout><Outlet /></Layout>
</ErrorBoundary>

i18n-ready

Сейчас — только русский. Но строки НЕ пишутся хардкодом в JSX.

// constants/strings.ts
export const strings = {
  nav: { ideas: "Идеи", new: "Новая", voice: "Голос" },
  auth: { login: "Войти", register: "Регистрация" },
} as const;
// Использование
<T path="auth.login" />
<T path="nav.ideas" />

При переходе на react-i18next — только замена реализации <T>, строки не трогать.

ESLint Ruleset (для CI и будущих проектов)

Плагины:

  • @eslint/js — базовые JS правила
  • typescript-eslint — TS strict ruleset
  • eslint-plugin-jsx-a11y — WCAG AA enforcement

Ключевые правила:

"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"jsx-a11y/alt-text": "error",
"jsx-a11y/aria-props": "error",
"jsx-a11y/aria-role": "error",
"jsx-a11y/label-has-associated-control": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/no-static-element-interactions": "warn",
"no-console": ["error", { allow: ["warn", "error"] }],
"prefer-const": "error",
"no-var": "error",

Установка в новом проекте:

npm install -D eslint @eslint/js typescript-eslint eslint-plugin-jsx-a11y

Testing (Vitest)

  • Имена файлов: *.test.ts / *.test.tsx рядом с тестируемым модулем.
  • Smoke: каждая страница рендерится без падения.
  • Store: каждый action тестируется (login, logout, create, delete).
  • Формы: заполнить → submit → проверить вызов API.
  • JSON-репортёр для QATesterAgent: vitest run --reporter=json.
  • Минимальный coverage: 30% (только критические пути: auth, forms, ErrorBoundary).

React Version Policy

  • Фиксирована: React 18.3.x LTS
  • Переход на React 19 — только когда:
    1. vite-plugin-pwa обновит поддержку
    2. Все зависимости совместимы
    3. Отдельный этап миграции

VPS Compatibility

Весь инструментарий (Zustand, react-hook-form, zod, Vitest, ESLint) — npm-пакеты, работают на любой OS. Сборка под VPS:

npm ci          # чистая установка под Ubuntu
npm run build   # Vite -> dist/