6.8 KiB
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 ruleseteslint-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 — только когда:
vite-plugin-pwaобновит поддержку- Все зависимости совместимы
- Отдельный этап миграции
VPS Compatibility
Весь инструментарий (Zustand, react-hook-form, zod, Vitest, ESLint) — npm-пакеты, работают на любой OS. Сборка под VPS:
npm ci # чистая установка под Ubuntu
npm run build # Vite -> dist/