159 lines
12 KiB
Markdown
159 lines
12 KiB
Markdown
# Frontend UI
|
||
|
||
## Выбор компонента и state
|
||
|
||
Сначала переиспользуй подходящий component проекта; иначе используй готовый
|
||
Mantine component и общую тему. Для выпадающего выбора используй `Select` или
|
||
`MultiSelect`, а не браузерный `<select>` / `NativeSelect` по умолчанию.
|
||
Нативный control или собственная реализация допустимы, когда готовые components
|
||
не покрывают конкретное продуктовое требование или доступность; укажи причину
|
||
в результате работы. Краткость кода и удобство тестирования не являются причиной:
|
||
ограничения JSDOM решай в test setup, сохраняя production UI.
|
||
Семантический HTML для структуры страницы остаётся уместным.
|
||
|
||
Wrapper нужен для проектной ответственности, а не ради нового имени
|
||
для Button. Локальный CSS module принадлежит component; общий token вводи для
|
||
реально общей роли через [design system](../../../docs/development/design-system.md).
|
||
|
||
| Состояние | Владелец |
|
||
| --- | --- |
|
||
| Ответ сервера / query lifecycle | TanStack Query, [data rules](frontend-data.md) |
|
||
| Ввод и ошибки формы | React Hook Form + Zod, container/VM |
|
||
| Параметр навигации, который должен переживать переход/ссылку | Router/URL |
|
||
| Локальное открытие, focus, rendering fallback | React UI state |
|
||
| Значение, вычисляемое из уже доступных inputs | Вычисление при render/в VM, без копирования через effect в state |
|
||
|
||
Effect нужен для синхронизации с внешней системой; действие пользователя
|
||
обрабатывай в handler. Не добавляй store, memoization или abstraction без текущей
|
||
потребности. Inspect ближайший component до создания нового pattern.
|
||
|
||
## Container, VM и view
|
||
|
||
Для data-driven finance page container вызывает один page VM и выбирает
|
||
Loading/Error/Empty/Loaded view. VM владеет data/auth/route inputs, state selection,
|
||
retry callbacks, domain grouping и derived presentation data. View получает props.
|
||
|
||
| Файл | Ответственность |
|
||
| --- | --- |
|
||
| `Component.tsx` | Container или публичный component |
|
||
| `Component.vm.ts` | Page VM, данные и действия |
|
||
| `Component.view.tsx` | Presentation |
|
||
| `Component.types.ts` | Переиспользуемые props/local types, когда отдельный owner полезен |
|
||
| `Component.module.css` | Локальные стили |
|
||
|
||
Во view нет data/auth/router/navigation/VM hooks и вычислений по domain collection.
|
||
Допустимы `useTranslation`, form-context hooks, UI-only state (например failed
|
||
image fallback) и formatter одного переданного значения. Группировка/доли
|
||
коллекции готовятся в VM. Перенос fetch во view ради меньшего container нарушает границу.
|
||
|
||
Отдельный state/section с самостоятельной responsibility выноси в flat
|
||
`components/*.view.tsx`. Небольшой tightly-coupled renderer оставляй рядом.
|
||
Не создавай folder/barrel/test/CSS для каждого view и не дроби по line count.
|
||
Пример: [AccountIncomePage](../../src/pages/account-income/ui/AccountIncomePage.tsx)
|
||
и [его VM](../../src/pages/account-income/ui/AccountIncomePage.vm.ts).
|
||
|
||
## Формы
|
||
|
||
Используй `react-hook-form`, `zodResolver`, `FormProvider`. Container/VM создаёт
|
||
form state, view получает `onSubmit`/`disabled`, fields используют `useFormContext`.
|
||
Во время submit блокируй все действия формы. Zod errors показывай у поля;
|
||
network error — отдельным Alert с сохранением ввода. Невалидная форма не отправляет request.
|
||
|
||
## Состояния и recovery
|
||
|
||
Без успешных данных выбирай loading → error → empty → loaded. При наличии query
|
||
данных сохраняй их во время refetch и его ошибки: progress, локальное предупреждение,
|
||
retry. Это Client Cached Snapshot, отдельный от server stale/fallback.
|
||
Known API errors принадлежат local view; непойманные route errors — route-keyed
|
||
ErrorBoundary. Сброс query cache после смены session/connection задают data rules.
|
||
|
||
Initial loading использует page-owned structural skeleton по semantic regions,
|
||
не одиночный spinner/StateView. Partial loading принадлежит section. Busy-область
|
||
доступна assistive technology, shapes декоративны, без данных, интерактивности
|
||
и focus. Route fallback сохраняет shell; до auth ready — neutral shell skeleton.
|
||
Button loading при mutation и компактный cached-snapshot progress остаются
|
||
контекстными indicators. Проверяй keyboard access к actions и безопасный retry.
|
||
|
||
## Код и components
|
||
|
||
Style/type automation задают configs. Используй `import type`; `enum` и
|
||
`namespace` в frontend не применяются. Named components — const-valued; Biome
|
||
проверяет declarations, no-default-export и exports-last в своём scope.
|
||
|
||
Обязательный review: `FC<Props>` и named props рядом/в types-файле; без inline
|
||
props typing и destructuring в параметрах. Принимай `props`, destructure в body.
|
||
Только children — `FC<PropsWithChildren>`; дополнительные props — named type
|
||
на основе `PropsWithChildren<{ ... }>`. Без props — `FC`.
|
||
Своди обычные exports к читаемому нижнему named block; barrel делает прямой
|
||
re-export без промежуточного import.
|
||
|
||
CSF `*.stories.tsx` сохраняют default meta и прямые story exports для indexer.
|
||
Vite/Vitest configs и `.storybook/main.ts`/`preview.tsx` имеют только
|
||
конфигурационное default-export exception. Scope исключений принадлежит biome.json.
|
||
|
||
## Domain literals and magic values
|
||
|
||
Finite business vocabulary must be modeled explicitly; incidental implementation
|
||
literals stay inline. Это относится к error codes, statuses, operation/account/
|
||
instrument/transaction types, permissions, roles, feature states и бизнес-режимам.
|
||
|
||
Перед новым определением найди existing owner/type/value. В branching и comparisons
|
||
используй символическое значение владельца; не повторяй raw domain literals.
|
||
Храни словарь у domain/module/entity с соблюдением границ слоёв, без глобальной
|
||
свалки `constants.ts`. Сохраняй принятый pattern; для нового project-owned словаря
|
||
без подходящего аналога используй `as const` с derived union:
|
||
|
||
```ts
|
||
const BankErrorCode = {
|
||
InvalidDate: "BANK_INVALID_DATE",
|
||
ImmutableOperation: "BANK_IMMUTABLE_OPERATION",
|
||
} as const;
|
||
type BankErrorCode = (typeof BankErrorCode)[keyof typeof BankErrorCode];
|
||
|
||
// Вместо code === "BANK_INVALID_DATE":
|
||
if (code === BankErrorCode.InvalidDate) { /* existing handling */ }
|
||
```
|
||
|
||
Значимые numeric policies (timeout, retry, page size, threshold, coefficient)
|
||
называй по смыслу и единицам: `REQUEST_TIMEOUT_MS`, `CACHE_TTL_SECONDS`,
|
||
`MAX_RETRY_COUNT`, `PAGE_SIZE`. Оставляй inline UI labels в принятом месте,
|
||
log messages, индексы, `index + 1`, `Math.max(value, 0)`, `page ?? 1`,
|
||
`percentage * 100` и очевидные локальные детали без semantic benefit.
|
||
При неясном owner/смысле оставь значение и укажи ограничение в review.
|
||
|
||
Не мигрируй корректные existing enum/union styles массово. Не добавляй ради
|
||
правила factories, registries, schemas, loaders, abstraction layers, dependencies,
|
||
custom lint/AST rules, generation/validation scripts или тесты существования
|
||
констант. Semantic review проверяет ownership, единое написание и неизменность
|
||
wire values; обычные behavior tests проверяют поведение.
|
||
|
||
Backend API vocabulary бери из generated OpenAPI types/client, runtime values —
|
||
когда генератор их предоставляет. Не создавай ручную копию backend enum/object.
|
||
Текущий `schema.d.ts` предоставляет только types: используй indexed-access type
|
||
из `paths`; typed literals в запросах/сравнениях допустимы при отсутствии
|
||
runtime values. `ProblemDetailsDto.code` сейчас `string`: не выдумывай закрытый
|
||
frontend error enum. Развитие API source — отдельное contract-решение.
|
||
Frontend `enum` по-прежнему запрещён локальным TS/style profile.
|
||
|
||
`noMagicNumbers` включён как error в Biome. Fixtures, tests и stories исключены
|
||
из этого правила; остальные проверки для них сохраняются. Используй имена
|
||
политик/единиц и existing values. Точечное подавление допустимо для очевидного
|
||
incidental значения с объяснением, например перевода доли в проценты; не подавляй
|
||
скрытые бизнес-лимиты. Semantic magic strings проверяются conventions/review: в
|
||
установленном Biome нет такого встроенного правила, новый инструмент не добавляем.
|
||
|
||
## Строки и валидация
|
||
|
||
Продуктовый текст хранится в `src/shared/i18n/locales/ru.ts`, читается через
|
||
`useTranslation()`. В Zod используй `validationMessage(key)` из i18n/zod, без
|
||
пользовательских literal messages. Это mandatory semantic review: общего запрета
|
||
непереведённых JSX-строк в Biome/TypeScript нет. Synthetic story fixtures и
|
||
описания историй допустимы; они не заменяют продуктовую локализацию.
|
||
|
||
## Проверка
|
||
|
||
Type/style: `npm run typecheck`, `npm run lint`. Для risk behavior/изменения тестов
|
||
используй [Testing](frontend-testing.md). UI: keyboard, narrow viewport,
|
||
loading/error/empty/loaded и recovery по применимости. [Storybook](../../../docs/development/ui-catalog.md)
|
||
уже установлен; используй его как reference, но сверяй UI с задачей отдельно от build.
|