docs: refine frontend conventions and routing
Frontend security / security (push) Successful in 8s
Frontend verify / verify (push) Successful in 3m56s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 3m41s
Conventional commits / commits (pull_request) Successful in 19s

This commit is contained in:
2026-09-15 05:42:14 +03:00
parent 0244acf429
commit 718365cd5a
10 changed files with 202 additions and 134 deletions
+7 -21
View File
@@ -1,23 +1,9 @@
# Frontend router
# Frontend conventions: вход
Читай строки, которые активирует задача, затронутый путь **или** обнаруженная
граница. Объединяй совпадения; якорь разрешает чтение только нужного раздела.
Не загружай все conventions из-за одного frontend-файла.
Актуальный routing по задаче, пути и boundary находится в [AGENTS](../AGENTS.md).
Выбирай только активированные темы; при новой границе повтори routing.
[Architecture](architecture.md) объясняет mental model, [SOURCES](SOURCES.md)
ведёт к системным фактам, specifications и ADR по конкретному вопросу.
| Решение / сигнал | Читать до решения |
| --- | --- |
| Создать/перенести module/component/slice; выбрать page/entity/feature/shared; cross-slice imports | [Architecture](conventions/frontend-architecture.md) |
| Component/props, container/VM/view, form, local state/effect, loading/error/retry, Mantine/UI | [UI](conventions/frontend-ui.md) |
| Только продуктовая строка или Zod message | [Строки](conventions/frontend-ui.md#строки-и-валидация) |
| TS declaration/import/export, domain literals, magic values | [Код](conventions/frontend-ui.md#код-и-components) + [Domain literals](conventions/frontend-ui.md#domain-literals-and-magic-values) |
| query/mutation, shared/api, server state/cache, generated types, Bearer/refresh | [Data](conventions/frontend-data.md) |
| Тесты или behavior с testing risk | [Testing](conventions/frontend-testing.md) |
| Новый abstraction/dependency, refactor или неоднозначный scope | [Общий выбор реализации](../../AGENTS.md) |
| Request/response field, route/status/error shape, OpenAPI snapshot/generated type | [Источник HTTP-контракта](../../docs/architecture/http-api.md#источник-контракта) |
| Login/refresh/logout, token/cookie, auth ownership | Data + [security](../../backend/.ai/conventions/security-auth.md) + Testing |
| Storybook/theme/tokens/shared UI | UI + точные [источники](SOURCES.md) дизайн-системы/каталога |
UI-only props/presentation требуют UI, без API/structure/testing при отсутствии
их triggers. Показ query state не означает изменение data fetching.
Изменение тестов активирует Testing и без production diff. Нужные project facts
выбирай через SOURCES. Повтори routing при новой границе и по итоговому diff.
Этот адрес сохранён для существующих входящих ссылок; второй копии правил
и таблицы маршрутов здесь нет.
+1 -1
View File
@@ -2,7 +2,7 @@
| Что нужно установить | Владелец |
| --- | --- |
| Какое implementation-решение принять | [CONVENTIONS](CONVENTIONS.md) |
| Какое implementation-решение принять | [AGENTS](../AGENTS.md) |
| HTTP wire contract и направление генерации | Backend-owned contract → provided snapshot → [Источник HTTP-контракта](../../docs/architecture/http-api.md#источник-контракта) |
| Error codes/validation/error envelope | [docs: HTTP API](../../docs/architecture/http-api.md) |
| Auth/session, token storage и refresh | [Аутентификация](../../docs/domains/authentication/overview.md), [подключение брокера](../../docs/integrations/tbank.md) |
+34
View File
@@ -0,0 +1,34 @@
# Frontend mental model
React + TypeScript + Vite SPA. Router задаёт navigation; providers в `app`
подключают auth, TanStack Query, i18n и Mantine. Vite использует React Compiler;
Vitest — отдельную JSDOM конфигурацию. Точные versions/scripts — в
[package.json](../package.json), реализация — source of truth.
```text
app → pages → features → entities → shared
page container → page VM → entity/feature query → shared/api → backend /api/v1
↓
props → state/section views
```
`widgets` не используется. Pages владеют route composition и локальным поведением;
features — самостоятельными возможностями пользователя; entities — устойчивыми
доменными понятиями; shared — техническими primitives без domain responsibility.
Правила выделения owner и public `index.ts` — в
[FSD convention](conventions/frontend-architecture.md).
Server state и lifecycle запроса принадлежат TanStack Query; VM собирает данные
для view. Transport — `openapi-fetch`/`openapi-react-query`, типизированные из
предоставленного snapshot через `openapi-typescript`. Backend владеет wire
contract; frontend не читает его checkout для генерации и не вызывает T-Bank.
[Data convention](conventions/frontend-data.md) задаёт auth/cache границы.
`shared/theme/tokens.json` — переносимый source design tokens; `theme.ts`
адаптирует доверенный JSON к Mantine без runtime schema. App и Storybook читают
public API `shared/theme`. Storybook показывает synthetic presentation states,
не использует application API. RTL/MSW проверяют поведение приложения отдельно.
Existing route-aware AppShell расположен в shared/ui; это legacy placement,
а не образец нового shared primitive. Общий план улучшений читается только
при migration через [router](CONVENTIONS.md).
+10 -11
View File
@@ -27,20 +27,17 @@ Slice именуется по ответственности, а не `helpers`,
Cross-slice consumer использует `@/` и `index.ts` владельца, например
`@/entities/portfolio`. Alias Vite и TypeScript согласован. Другой slice не
импортирует внутренний файл напрямую. Направление зависимостей проверяет
импортирует внутренний файл напрямую. Slices `pages`, `features`, `entities` не зависят от соседнего slice своего слоя;
композиция принадлежит вышележащему owner. Existing меж-slice исключения
не являются разрешением добавлять новые.
Направление зависимостей и public imports частично проверяет
`npm run deps`; Knip проверяет использование exports, а не смысл placement.
## UI foundation
Общий visual foundation принадлежит `shared/theme`. `tokens.json` — переносимый
WealthLedger v1 source; `theme.ts` адаптирует доверенный JSON в Mantine без runtime
schema. App и Storybook используют public API `shared/theme/index.ts`: theme,
designTokens, color roles, CSSVariablesResolver и base styles. Внешний consumer
читает JSON напрямую. Mantine defaults и локальные domain styles остаются у владельцев.
`shared/ui` содержит StateView/LoadingSkeleton. Existing route-aware AppShell также
лежит в shared/ui, но не является общим catalog primitive; его расположение не
служит основанием переносить новую domain/navigation логику в shared.
Изменяя foundation, прочитай [design system](../../../docs/design/overview.md).
Mental model, theme и data flow — в [architecture](../architecture.md).
При изменении foundation прочитай [design system](../../../docs/design/overview.md).
## Примеры
@@ -52,5 +49,7 @@ designTokens, color roles, CSSVariablesResolver и base styles. Внешний c
После placement/import change: `npm run deps`, `npm run typecheck`, `npm run knip`;
после собственного behavior — применимые tests. Финальный gate указан в AGENTS.
Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает
Existing dependency config не запрещает все same-layer imports: их и смысл
owner проверяет review. Не объявляй Steiger установленным; graph проверяет
текущий dependency-cruiser. Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает
правильный выбор слоя.
+21 -1
View File
@@ -23,7 +23,9 @@ type User = paths["/api/v1/auth/me"]["get"]["responses"][200]["content"]["applic
```
Domain/UI type допустим, когда он отличается по смыслу, а не копирует transport DTO.
Generated TS вручную не исправляется. Frontend-команды не читают соседний
Generated TS вручную не исправляется. Не добавляй параллельный fetch/axios
client, runtime schema, hash/checksum registry или дополнительный drift checker
без отдельной текущей потребности; existing OpenAPI flow уже владеет генерацией. Frontend-команды не читают соседний
backend checkout и не проверяют backend runtime OpenAPI; изменение wire
shape/status/route сохраняет
[границу HTTP-контракта](../../../docs/architecture/http-api.md#источник-контракта).
@@ -44,11 +46,29 @@ session-scoped cache. При auth/token change читай [security](../../../ba
GET subtree; ошибка mutation не меняет generation/cache. Это отдельная граница
от обычного фонового refetch: [connection policy](../../../docs/integrations/tbank.md#граница-финансового-кэша).
Mutations используют существующий generated client и query-key owner;
после успеха обновляй/invalidируй только связанные данные. Optimistic update
нужен конкретному UX и обязан иметь rollback/error behavior.
Default focus refetch выключен; freshness и retry определяются существующей
[query policy](../../../docs/adr/007-react-query-fetch-policy.md) и feature source.
При обычном refetch сохраняй Client Cached Snapshot, при session/connection
смене не показывай данные прежнего владельца. UI presentation — в UI convention.
## Errors
Извлекай code через existing `apiErrorCode` из shared/api. Backend Problem
Details code задаёт machine behavior; user-facing текст принадлежит i18n.
Network/unknown error получает безопасное локальное сообщение и retry,
не raw exception text. Input/domain failure сохраняет ввод и показывает
ошибку у ответственного form/section owner; не маскируй её успешным результатом.
Если одна группа API codes требует одинакового recovery в нескольких views,
сведи классификацию у ближайшего общего owner. Не размножай magic comparisons
и не объявляй закрытый frontend error enum: generated `code` пока `string`.
Неизвестный новый code должен сохранять fallback. Рефакторинг существующих
разных recovery paths требует behavior proof, а не общей обработки всех ошибок.
## Примеры и проверка
[usePortfolio](../../src/entities/portfolio/api/usePortfolio.ts) — entity query;
+5
View File
@@ -1,5 +1,9 @@
# Frontend testing
Перед нетривиальными тестами сформулируй
[общий Test Intent](../../../.ai/conventions/testing.md); отдельный документ
не требуется. Trivial low-risk изменения обходятся без него.
## Выбор проверки
Читай при изменении тестов, доступа, денежных расчётов, восстановления
@@ -11,6 +15,7 @@
| Чистое domain преобразование/денежный расчёт | Unit case с граничными значениями |
| Форма, route access, recovery, UI-state selection | RTL component/route test через видимое поведение |
| Headers, refresh/retry, query invalidation | MSW на transport boundary |
| Критический browser flow, который JSDOM не доказывает | Browser/e2e proof существующими доступными средствами; отдельный frontend e2e runner пока не принят |
| Только declarative props/Storybook fixture | Type/build + semantic/visual review; unit test без собственной логики не нужен |
Tests colocated с source (`*.test.tsx` для UI, существующий `*.test.ts` для pure code).
+15 -67
View File
@@ -11,6 +11,11 @@ Mantine component и общую тему. Для выпадающего выбо
ограничения JSDOM решай в test setup, сохраняя production UI.
Семантический HTML для структуры страницы остаётся уместным.
Icons ищи у существующего owner (например,
[BankOperationIcon](../../src/entities/bank-card/ui/BankOperationIcon.tsx));
общая icon library сейчас не принята. Не добавляй пакет ради одного символа. Business/domain
components живут у feature/entity/page, даже при повторном использовании.
Wrapper нужен для проектной ответственности, а не ради нового имени
для Button. Локальный CSS module принадлежит component; общий token вводи для
реально общей роли через [design system](../../../docs/design/overview.md).
@@ -23,9 +28,11 @@ Wrapper нужен для проектной ответственности, а
| Локальное открытие, focus, rendering fallback | React UI state |
| Значение, вычисляемое из уже доступных inputs | Вычисление при render/в VM, без копирования через effect в state |
Effect нужен для синхронизации с внешней системой; действие пользователя
обрабатывай в handler. Не добавляй store, memoization или abstraction без текущей
потребности. Inspect ближайший component до создания нового pattern.
State, который нужен в ссылке, back/forward или после navigation, хранится в URL
через existing router search params. Остальной UI state остаётся у ближайшего
owner. Shared client store нужен только нескольким consumers без подходящего
общего owner; наличие двух компонентов само по себе его не требует.
Hooks/effects/derived state — по [TypeScript/React](typescript-react.md#hooks-и-derived-state).
## Container, VM и view
@@ -76,71 +83,12 @@ Button loading при mutation и компактный cached-snapshot progress
## Код и 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.
Types, props, hooks и memoization — в [TypeScript/React](typescript-react.md).
## 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 нет такого встроенного правила, новый инструмент не добавляем.
Правило и generated API exception — в
[TypeScript/React](typescript-react.md#domain-literals-and-magic-values).
## Строки и валидация
@@ -154,5 +102,5 @@ incidental значения с объяснением, например пере
Type/style: `npm run typecheck`, `npm run lint`. Для risk behavior/изменения тестов
используй [Testing](frontend-testing.md). UI: keyboard, narrow viewport,
loading/error/empty/loaded и recovery по применимости. [Storybook](../../README.md#ui-каталог-storybook)
уже установлен; используй его как reference, но сверяй UI с задачей отдельно от build.
loading/error/empty/loaded и recovery по применимости. [Storybook](storybook.md) задаёт выбор stories и изоляцию каталога;
сверяй UI с задачей отдельно от build.
+33
View File
@@ -0,0 +1,33 @@
# Storybook
## Когда нужна story
- Shared UI/theme: показывай принятые visual roles и переиспользуемое поведение.
- Domain/page presentation: добавляй story, когда независимый visual state
нужен для разработки/review или является устойчивым UI pattern.
- Выбирай значимые состояния: default, loading, disabled, error, empty,
длинный контент и смысловые variants по применимости. Не строй декартово
произведение props и story для каждого trivial wrapper/helper.
- Fetch/auth/router container и VM проверяй application tests; story получает
synthetic props presentation component. Каталог не нужен каждому файлу.
## Изоляция и interaction
Используй CSF, `Meta`/`StoryObj`, `satisfies Meta`, локальные fixtures и actions.
Общие Mantine/theme/i18n/Dates providers уже есть в
[preview](../../.storybook/preview.tsx); не создавай второй theme source.
Stories не достигают application API, auth, VM и query providers даже транзитивно;
границу проверяет `deps:storybook`. Не добавляй MSW worker ради каталога.
`play` через `storybook/test` нужен, когда interaction иллюстрирует собственный
контракт: keyboard activation, доступность действия, local callback. Не повторяй
RTL suite и не тестируй внутренности Mantine. Существующий
[Button](../../src/shared/ui/Button.stories.tsx) показывает themed control
без production wrapper; [LoadingSkeleton](../../src/shared/ui/LoadingSkeleton.stories.tsx)
— значимые busy states.
`npm run typecheck`, `npm run deps:storybook`, `npm run build:catalog` проверяют
типы, изоляцию и сборку. Build не выполняет `play` как CI interaction suite;
для проверки interaction открой story в браузере, для regression используй
существующий [Testing](frontend-testing.md). Визуальное соответствие задаче,
keyboard и узкий viewport проверяются отдельно от успешной сборки.
+53
View File
@@ -0,0 +1,53 @@
# TypeScript и React
## Types и components
- Сохраняй strict TS profile и `import type`. Для недоверенного значения используй
`unknown` с narrowing; `any` не заменяет DTO/validation. Локальное исключение
на нетипизированной boundary должно быть узким и объяснённым.
- `enum` и `namespace` не используются: frontend имеет `erasableSyntaxOnly`.
Для состояния с разными payload выбирай discriminated union; не добавляй
необязательные поля, допускающие невозможные комбинации.
- Named components — const-valued `FC<Props>`; named props находятся рядом или
в types-файле. Принимай `props`, destructure в body, без inline props typing.
Только children — `FC<PropsWithChildren>`; дополнительные props — named type
на основе `PropsWithChildren<{ … }>`; без props — `FC`.
- Обычные exports — нижний named block; barrel делает прямой re-export.
CSF сохраняет default meta и прямые story exports; configuration exceptions
заданы в [Biome](../../biome.json). Не распространяй их на application code.
## Hooks и derived state
Hooks вызываются без условий и ранних выходов перед ними; custom hook нужен
связному поведению, а не новому имени над одним вызовом. Derived value вычисляй
при render/в VM. Пользовательское действие обрабатывай handler; `useEffect`
оставляй синхронизации с внешней системой с корректным cleanup/dependencies.
`useMemo`, `useCallback`, `memo` нужны измеренной стоимости или конкретному
контракту стабильной identity; не добавляй их ко всем значениям/handlers.
React Compiler включён в Vite, но не доказывает выигрыш и не заменяет effect
correctness. Не удаляй существующую memoization без понимания её consumers.
Container/VM/view и state owners — в [UI](frontend-ui.md).
## Domain literals and magic values
Общее правило ownership и символических значений —
[Domain values](../../../.ai/conventions/code-health.md#domain-values).
Backend vocabulary бери из generated `paths`, не создавай ручную копию enum.
Текущий `schema.d.ts` содержит только types: typed wire literals допустимы,
когда generator не предоставляет runtime values.
`ProblemDetailsDto.code` сейчас `string`. Не объявляй закрытый frontend error
enum и не превращай неизвестный code в известный через cast. Повторяющуюся
обработку своди у ответственного owner; [errors](frontend-data.md#errors).
`noMagicNumbers` в Biome — error; fixtures/tests/stories исключены только
из этого правила. Policy получает имя и единицы; подавление допустимо для
incidental value с объяснением. Обычный UI-текст не превращай в constant:
его owner — [i18n](frontend-ui.md#строки-и-валидация).
Semantic magic strings и props profile требуют review.
## Проверка
`npm run typecheck` и `npm run lint`; behavior проверяй по
[Testing](frontend-testing.md), placement — по [FSD](frontend-architecture.md).