Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s
194 lines
15 KiB
Markdown
194 lines
15 KiB
Markdown
# 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](data.md) |
|
||
| Ввод и ошибки формы | 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](typescript-react.md#hooks-и-derived-state).
|
||
|
||
## 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](../../src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.tsx)
|
||
и [его VM](../../src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts).
|
||
|
||
## Размещение файлов компонента
|
||
|
||
Правило действует в `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](../../src/entities/bank-card/ui/BankCardForm/BankCardForm.tsx)
|
||
лежит рядом со своим 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](typescript-react.md).
|
||
|
||
## Domain literals and magic values
|
||
|
||
Правило и generated API exception — в
|
||
[TypeScript/React](typescript-react.md#domain-literals-and-magic-values).
|
||
|
||
## Строки и валидация
|
||
|
||
Продуктовый текст хранится в `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](testing.md). UI: keyboard, narrow viewport,
|
||
loading/error/empty/loaded и recovery по применимости. [Storybook](storybook.md) задаёт выбор 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`; вывод о разделении фаз
|
||
банковских форм на этот сценарий автоматически не распространяется.
|