Enable every plugin recommended preset and replace Biome with ESLint formatting. Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
12 KiB
Frontend UI
Выбор компонента и state
Сначала переиспользуй подходящий component проекта; иначе используй готовый
Mantine component и общую тему. Для выпадающего выбора используй Select или
MultiSelect, а не браузерный <select> / NativeSelect по умолчанию.
Нативный control или собственная реализация допустимы, когда готовые components
не покрывают конкретное продуктовое требование или доступность; укажи причину
в результате работы. Краткость кода и удобство тестирования не являются причиной:
ограничения JSDOM решай в test setup, сохраняя production UI.
Семантический HTML для структуры страницы остаётся уместным.
Icons ищи у существующего owner (например,
BankOperationIcon);
статичные Add/Edit/Delete доступны в @wealthledger/icons. Их SVG декоративны
по умолчанию; имя action принадлежит кнопке. Standalone meaningful icon получает
aria-hidden={false}, role="img" и доступное имя. Category/kind mappings
и анимации остаются в entity. Не добавляй пакет ради одного символа. Business/domain
components живут у feature/entity/page, даже при повторном использовании.
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 нет 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 выноси в
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.
Формы
Используй 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; вывод о разделении фаз
банковских форм на этот сценарий автоматически не распространяется.