Files
frontend/.ai/conventions/ui.md
T
ksv741 ca0ad4f824
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s
refactor: enforce frontend view model boundaries
2026-09-28 18:48:23 +03:00

14 KiB
Raw Blame History

Frontend UI

Выбор компонента и state

Сначала переиспользуй подходящий component проекта; иначе используй готовый Mantine component и общую тему. Для выпадающего выбора используй Select или MultiSelect, а не браузерный <select> / NativeSelect по умолчанию. Нативный control или собственная реализация допустимы, когда готовые components не покрывают конкретное продуктовое требование или доступность; укажи причину в результате работы. Краткость кода и удобство тестирования не являются причиной: ограничения JSDOM решай в test setup, сохраняя production UI. Семантический HTML для структуры страницы остаётся уместным.

Icons импортируй из @wealthledger/icons; геометрия, SVG assets и animation lifecycle принадлежат этому пакету. Domain/category lookup возвращает параметры иконки из model сущности. SVG декоративны по умолчанию; доступное имя action принадлежит кнопке. Meaningful icon получает aria-hidden={false}, role="img" и доступное имя. Приложение и Storybook подключают CSS пакета.

Wrapper нужен для проектной ответственности, а не ради нового имени для Button. Локальный CSS module принадлежит component; общий token вводи для реально общей роли в визуальной основе.

Состояние Владелец
Ответ сервера / query lifecycle TanStack Query, data rules
Ввод и ошибки формы React Hook Form + Zod, container/VM
Параметр навигации, который должен переживать переход/ссылку Router/URL
Локальное открытие, focus, rendering fallback React UI state
Значение, вычисляемое из уже доступных inputs Вычисление при render/в VM, без копирования через effect в state

State, который нужен в ссылке, back/forward или после navigation, хранится в URL через existing router search params. Остальной UI state остаётся у ближайшего owner. Shared client store нужен только нескольким consumers без подходящего общего owner; наличие двух компонентов само по себе его не требует. Hooks/effects/derived state — по TypeScript/React.

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 получает готовые данные и именованные действия. Container вызывает VM; VM владеет запросами, формой, состоянием, обработчиками и derived values. Во view нет data/auth/router/navigation hooks и domain grouping. Data VM вызывает container; presentation hook не должен скрывать fetching во view. Presentation VM для form-context/UI-only state может использоваться автономно в каталоге: её граф зависимостей не достигает application data/API. useTranslation и декларативный вывод коллекции допустимы во view.

JSX props получают ссылки на функции, включая onClick, render props, dateParser и callbacks внутри объектов props. Для React Hook Form предпочитай useController в VM вместо inline Controller.render. Значение prop с условием сначала получает предметное имя: projectedAmount, archiveActionLabel. Проверяй narrowing при переносе: выражение не должно читать отсутствующие данные до ветки, которая раньше защищала доступ; обработчик сохраняет актуальный snapshot.

Внутри компонента нет именованных helpers. Чистые функции форматирования и адаптации UI inputs выноси в shared/lib, передавая явные аргументы. Domain преобразование остаётся в model своего owner; shared не импортирует entities/pages. Closure над состоянием, mutations и refs является действием VM, а не shared helper. Render-helper замени самостоятельным presentation component у того же owner.

Отдельный state/section с самостоятельной responsibility выноси в components/ своего component. Небольшой tightly-coupled renderer оставляй рядом; размер файла сам по себе не требует разделения. Пример: AccountIncomePage и его VM.

Размещение файлов компонента

Правило действует в src и packages: компонент с сопутствующими файлами (view, VM, types, CSS, tests, stories) размещай в папке Component/. Сохраняй полные имена файлов: Component.tsx, Component.view.tsx, Component.test.tsx. Создавай только нужные файлы; одиночный компонент оставляй непосредственно в ui, components или source-каталоге пакета.

Внутренние части принадлежат папке родителя; для набора state/section views используй её components/. Внутри него действует то же правило: отдельная папка объединяет комплект файлов, одиночный view остаётся файлом. Стили и хуки нескольких соседних компонентов остаются у ближайшего общего владельца; не копируй их и не прячь внутрь одного потребителя.

Папка компонента не создаёт новую FSD-границу. Публичные exports остаются в существующем index.ts slice/segment или entry пакета; внутри используй прямые относительные imports без дополнительных barrels на каждый компонент. При переносе обновляй imports, ссылки документации и проверки, зависящие от пути.

Пример: BankCardForm лежит рядом со своим view, тестом и story; общий BankForms.module.css остаётся в bank-card/ui.

Формы

Схемы Zod объявляй в *.schema.ts, включая .refine/.superRefine. Зависимая от текущих данных схема — фабрика с явными inputs в том же файле; VM вызывает фабрику. Сохраняй актуальность данных и deferred validation messages.

Используй 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

Types, props, hooks и memoization — в TypeScript/React.

Domain literals and magic values

Правило и generated API exception — в TypeScript/React.

Строки и валидация

Продуктовый текст хранится в src/shared/i18n/locales/ru.ts, читается через useTranslation(). В Zod используй validationMessage(key) из i18n/zod, без пользовательских literal messages. Это mandatory semantic review: общего запрета непереведённых JSX-строк в ESLint/TypeScript нет. Synthetic story fixtures и описания историй допустимы; они не заменяют продуктовую локализацию.

Проверка

Type/style: npm run typecheck, npm run lint. Для risk behavior/изменения тестов используй Testing. UI: keyboard, narrow viewport, loading/error/empty/loaded и recovery по применимости. Storybook задаёт выбор stories и изоляцию каталога; сверяй UI с задачей отдельно от build.

Визуальная основа

Private workspace packages/tokens владеет переносимыми значениями tokens.json; packages/ui/src/theme.ts читает export пакета @wealthledger/tokens/tokens.json и адаптирует его к Mantine. Используй @wealthledger/ui/theme и его semantic roles; runtime подключает @wealthledger/ui/styles.css после Mantine CSS. Providers остаются у runtime. Палитра page/component следует этим semantic roles. Поддерживается светлая схема. Цвет дополняет подпись или знак; проверяй контраст конкретной пары text/background. Warning не используй для мелкого текста на белом. Деньги сохраняют валюту, знак и различие между нулём и неизвестной суммой. Общие состояния демонстрирует Storybook; presentation stories используют synthetic data без application API.

EntryCard, StateView и LoadingSkeleton принадлежат packages/ui; публичный import — @wealthledger/ui. Переводы, retry и формы skeleton остаются у callers. Compatibility re-exports удалены; callers используют public export пакета.

Modal animation

BankCardForm, OperationForm, DeleteOperation и CategoryManager монтируют Modal с opened=true: Mantine Transition начинает в состоянии entered. CSS modal-appear/modal-backdrop-appear в packages/ui/src/styles.css обеспечивает появление при mount (220ms, только no-preference). Mantine theme владеет exit transition (180ms), overlay blur и completion callback; onExitTransitionEnd позволяет дождаться закрытия перед unmount и вернуть фокус. Удаление CSS без изменения mount lifecycle убирает вступительную анимацию. TbankConnection Modal использует toggle opened; вывод о разделении фаз банковских форм на этот сценарий автоматически не распространяется.