docs(ai): route frontend conventions by task
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# Frontend architecture
|
||||
|
||||
## Выбор владельца
|
||||
|
||||
Сначала найди владельца поведения и ближайший пример. Расширяй existing slice,
|
||||
если ответственность остаётся связной. Размер файла или возможный будущий reuse
|
||||
не создают новую архитектурную границу.
|
||||
|
||||
| Ответственность | Размещение |
|
||||
| --- | --- |
|
||||
| Bootstrap, глобальные providers, routes | `app` |
|
||||
| Композиция маршрута и поведение, нужное только этому экрану | `pages/<page>` |
|
||||
| Самостоятельная пользовательская возможность с собственным смыслом | `features/<capability>` |
|
||||
| Представление/поведение бизнес-сущности | `entities/<entity>` |
|
||||
| Переиспользуемый код без WealthLedger domain meaning | `shared` |
|
||||
|
||||
Локальный FSD profile: `app → pages → features → entities → shared`, без `widgets`.
|
||||
Крупный уникальный блок остаётся в своей page. Form, mutation или click handler
|
||||
сами по себе не требуют feature. Два callers не делают domain helper generic shared.
|
||||
Slice именуется по ответственности, а не `helpers`, `hooks` или `types`.
|
||||
|
||||
**Безопасный путь:** при сомнении оставь код у ближайшего допустимого owner.
|
||||
Извлекай его, когда появилась самостоятельная ответственность или реальный reuse.
|
||||
Не вводи новый layer и не обходи public API, чтобы устранить неудобный import.
|
||||
|
||||
## Imports и public API
|
||||
|
||||
Cross-slice consumer использует `@/` и `index.ts` владельца, например
|
||||
`@/entities/portfolio`. Alias Vite и TypeScript согласован. Другой slice не
|
||||
импортирует внутренний файл напрямую. Направление зависимостей проверяет
|
||||
`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/development/design-system.md).
|
||||
|
||||
## Примеры
|
||||
|
||||
- Общая busy-область: существующий [LoadingSkeleton](../../src/shared/ui/LoadingSkeleton.tsx), а не domain widget в shared.
|
||||
- Account-income state views: [страница](../../src/pages/account-income/ui/AccountIncomePage.tsx) и её flat `components/*.view.tsx`; новый page-local filter остаётся здесь.
|
||||
- Portfolio query: [entity public API](../../src/entities/portfolio/index.ts); новая UI-сводка с бизнес-смыслом не становится shared только из-за нескольких экранов.
|
||||
|
||||
## Проверка
|
||||
|
||||
После placement/import change: `npm run deps`, `npm run typecheck`, `npm run knip`;
|
||||
после собственного behavior — применимые tests. Финальный gate указан в AGENTS.
|
||||
Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает
|
||||
правильный выбор слоя.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Frontend data
|
||||
|
||||
## Выбор owner
|
||||
|
||||
Server state принадлежит TanStack Query, form state — React Hook Form, локальное
|
||||
UI — React state. Не копируй успешный query result в useState/context для второго
|
||||
кэша. Domain-specific queries, keys и policies принадлежат вызывающему slice;
|
||||
общий transport — `shared/api`. Page VM выбирает inputs и собирает presentation data.
|
||||
|
||||
Используй `apiClient`/`apiQueryClient` из shared public API. Прямой fetch в
|
||||
pages/features/entities и прямой T-Bank call запрещены. Сначала найди
|
||||
существующий query того же endpoint; расширяй его owner, не создавай второй key
|
||||
или глобальную query policy для одной страницы.
|
||||
|
||||
## Типы и contract
|
||||
|
||||
Backend runtime OpenAPI — source; committed frontend snapshot и generated
|
||||
`schema.d.ts` производны. Тип request/response бери из generated `paths`, например:
|
||||
|
||||
```ts
|
||||
type User = paths["/api/v1/auth/me"]["get"]["responses"][200]["content"]["application/json"];
|
||||
```
|
||||
|
||||
Domain/UI type допустим, когда он отличается по смыслу, а не копирует transport DTO.
|
||||
Generated TS вручную не исправляется. Изменение wire shape/status/route проходит
|
||||
[общий contract workflow](../../../docs/development/runtime-openapi-verification.md).
|
||||
Одна UI-модель не требует менять API. `openapi:generate` работает offline от snapshot;
|
||||
`openapi:check` генерирует во временный каталог и сравнивает, не меняя tracked files.
|
||||
|
||||
## Auth и invalidation
|
||||
|
||||
Bearer header принадлежит shared authenticated fetch; query/mutation не задают
|
||||
Authorization. Auth endpoints исключены из Bearer/retry middleware. Refresh
|
||||
координируется single-flight; после `401` с `UNAUTHORIZED` возможен один retry.
|
||||
Не добавляй собственный refresh/retry loop в caller.
|
||||
|
||||
Protected queries используют existing session-aware boundary; logout очищает
|
||||
session-scoped cache. При auth/token change читай [security](../../../backend/.ai/conventions/security-auth.md)
|
||||
и [authentication](../../../docs/features/authentication.md).
|
||||
Успешная connection mutation коммитит подтверждённый status и сбрасывает financial
|
||||
GET subtree; ошибка mutation не меняет generation/cache. Это отдельная граница
|
||||
от обычного фонового refetch: [connection policy](../../../docs/features/tbank-connection.md#граница-финансового-кэша).
|
||||
|
||||
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.
|
||||
|
||||
## Примеры и проверка
|
||||
|
||||
[usePortfolio](../../src/entities/portfolio/api/usePortfolio.ts) — entity query;
|
||||
[shared client](../../src/shared/api/client.ts) — transport/auth boundary.
|
||||
Изменяя header/retry/invalidation, проверяй наблюдаемое поведение через MSW и
|
||||
[Testing](frontend-testing.md); не обращайся к реальному T-Bank.
|
||||
`npm run openapi:check`, `npm run typecheck` и targeted tests проверяют разные
|
||||
части решения. Для API semantics нужны также exact docs и contract gate.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Frontend testing
|
||||
|
||||
## Выбор проверки
|
||||
|
||||
Читай при изменении тестов или behavior с [testing risk](../../../docs/development/engineering-governance.md#risk-based-testing).
|
||||
Выбирай boundary по наблюдаемому исходу, а не внутреннему helper/component state.
|
||||
|
||||
| Изменение | Дешёвая надёжная проверка |
|
||||
| --- | --- |
|
||||
| Чистое domain преобразование/денежный расчёт | Unit case с граничными значениями |
|
||||
| Форма, route access, recovery, UI-state selection | RTL component/route test через видимое поведение |
|
||||
| Headers, refresh/retry, query invalidation | MSW на transport boundary |
|
||||
| Только declarative props/Storybook fixture | Type/build + semantic/visual review; unit test без собственной логики не нужен |
|
||||
|
||||
Tests colocated с source (`*.test.tsx` для UI, существующий `*.test.ts` для pure code).
|
||||
Arrange/Act/Assert, semantic queries (`getByRole`, label); test id — когда
|
||||
пользовательский selector не выражает нужный элемент. Не тестируй Mantine/React
|
||||
internals, private calls или passthrough wrapper ради coverage.
|
||||
|
||||
## Инструменты и isolation
|
||||
|
||||
Используй `fireEvent` из объявленного `@testing-library/react` и ожидай async
|
||||
outcome через `findBy*`/`waitFor`. `user-event` доступен транзитивно через Storybook,
|
||||
но не принят как direct dependency; не добавляй его ради старого Markdown.
|
||||
MSW запускается только Vitest setup в `src/test/`, без browser worker/mock dev mode.
|
||||
Проверяй сеть MSW handlers, а не mock внутренностей query client или реальный backend.
|
||||
|
||||
Независимый case получает свой `createTestQueryClient`; rerender сохраняет
|
||||
экземпляр и seeded cache. Providers выбирай явно. Existing teardown выполняет
|
||||
RTL cleanup, auth reset и MSW reset: [test client](../../src/test/query.tsx).
|
||||
|
||||
## Важные случаи
|
||||
|
||||
Форма: field errors, отсутствие request при invalid input, submit blocking,
|
||||
сохранение ввода при network error. Routes: доступ/redirect и neutral auth bootstrap.
|
||||
Finance: loading/error/empty/loaded, cached snapshot при refetch failure, retry,
|
||||
отсутствие старых данных после session/connection switch. ErrorBoundary: test-only
|
||||
throwing route, безопасная ссылка и reset после navigation.
|
||||
|
||||
Примеры: [AuthForm test](../../src/features/auth/ui/AuthForm.test.tsx),
|
||||
[client test](../../src/shared/api/client.test.ts),
|
||||
[usePortfolio test](../../src/entities/portfolio/api/usePortfolio.test.tsx).
|
||||
Не копируй fixtures/сценарии целиком, если задача затрагивает один исход.
|
||||
|
||||
## Команды
|
||||
|
||||
Итерация: `npm run test:run -- <path>` без coverage. Полный `npm run verify`
|
||||
запускает Vitest один раз через `test:coverage`. Coverage floors — anti-regression
|
||||
constraint, не повод тестировать декларации. После UI изменения отдельно
|
||||
проверь доступность/визуальные состояния, которых JSDOM не доказывает.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Frontend UI
|
||||
|
||||
## Выбор компонента и state
|
||||
|
||||
Используй существующий Mantine component и общую тему, если они выражают нужное
|
||||
поведение. 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.
|
||||
|
||||
## Строки и валидация
|
||||
|
||||
Продуктовый текст хранится в `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.
|
||||
Reference in New Issue
Block a user