15 KiB
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 хранится цельным декларативным
компонентом: не разбивай path-строки и не собирай геометрию в runtime.
Русские category names из API — данные для нормализованного сопоставления,
не ключи реестра иконок; неизвестные/переименованные категории имеют fallback.
Известные названия банков принадлежат enum BankName в bank-card 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.
VM собирает связные предметные hooks, когда внутри смешиваются независимые
сценарии. Например, useOpeningDateField владеет draft/date parsing и событиями
даты, usePaymentSystemField — выбором платёжной системы. Каждый такой hook
возвращает данные и действия своей ответственности, VM объединяет их для view.
Не группируй несвязанные функции только по префиксу on* и не создавай
passthrough hook над одним вызовом ради формального дробления.
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; вывод о разделении фаз
банковских форм на этот сценарий автоматически не распространяется.