12 KiB
Context
Frontend уже разделяет запросные экраны на container, ViewModel и отдельные представления состояний. ViewModel отличает начальную загрузку без данных от фонового обновления Client Cached Snapshot, а pending-состояния действий остаются у соответствующих форм и controls. Сейчас эти разные состояния визуально смешаны: top-level loading views и route fallback используют общий spinner, некоторые частичные блоки показывают loading-текст, а auth guards до завершения bootstrap не рендерят ничего.
Mantine уже является UI dependency и предоставляет Skeleton, layout и progress primitives. Поэтому изменение должно остаться в presentation boundaries, сохранить текущие ViewModel, query/cache и auth transitions и не вводить новый loading framework.
Goals / Non-Goals
Goals:
- Сделать initial, partial, lazy-route и auth-bootstrap loading states структурно похожими на ожидаемую область содержимого.
- Сохранить единое доступное обозначение занятой области и исключить skeleton placeholders из интерактивной и focus-модели.
- Оставить уже показанные данные и controls основным контекстом для cached refresh и pending mutations.
- Удержать ответственность за форму skeleton рядом с соответствующим app, page или section view.
Non-Goals:
- Изменение запросов, cache timing, state selection, auth/route decisions, error/empty states или доменных данных.
- Замена компактной индикации внутри pending controls и рядом с видимым Client Cached Snapshot.
- Новый UI framework, глобальная orchestration загрузки или редизайн loaded screens.
- Изменение backend, API contract или persistence.
Decisions
1. Loading presentation принадлежит существующим view boundaries
Top-level page loading остаётся отдельным state view, выбранным существующим container по состоянию ViewModel. Каждый такой view строит skeleton из тех же типов layout regions, что и соответствующий loaded screen: heading, cards, filters, summaries или rows. Частичный skeleton принадлежит section view и занимает только незагруженный блок, поэтому соседнее готовое содержимое не скрывается.
Это сохраняет установленную границу container -> ViewModel -> view и позволяет
форме placeholder следовать реальной семантике экрана. Альтернатива — заменить
общий loading StateView одним универсальным skeleton — отклонена: универсальная
форма не может одновременно правдоподобно представлять список счетов,
Portfolio, ledger и локальные sections и быстро разойдётся с loaded layouts.
Общий StateView сохраняется для error и empty outcomes, но перестаёт владеть initial loading. Его loading-вариант удаляется после перевода всех consumers, чтобы spinner-based primary state нельзя было случайно продолжать использовать.
2. Используются установленные Mantine primitives без новой абстракции
Skeleton shapes собираются из Mantine Skeleton и текущих layout primitives. Занятая область получает единый доступный status/busy contract и локализованное имя, а сами визуальные placeholders остаются декоративными, не содержат fake values и не создают focus targets. Custom animation не добавляется.
Низкоуровневая shared-обёртка допустима только для повторяющейся busy-семантики; структура cards, sections и rows остаётся в page/entity presentation layers. Альтернативы в виде собственной CSS skeleton-системы или новой dependency отклонены, потому что установленная библиотека уже покрывает нужные primitives и снижает surface поддержки.
3. Route loading и auth bootstrap остаются app-level concerns
Suspense boundaries в routing layer получают neutral structural fallbacks для ожидаемой области: auth-card shape для public auth routes, application-content shape внутри доступного shell и neutral shell shape там, где сам shell ещё не доступен. Уже отрисованный application shell не заменяется при загрузке дочернего route chunk.
Auth guards продолжают принимать решение только по существующему ready и
user. Пока ready ложен, они показывают neutral shell skeleton без profile,
navigation result или route content; после bootstrap текущий redirect/render
flow продолжается без изменений. Перенос bootstrap в router data APIs или
изменение auth ownership отклонены как несоразмерные визуальной задаче.
4. Contextual progress не переводится на skeleton
Button loading для submit/remove/update и компактные indicators фонового обновления сохраняются. Когда Client Cached Snapshot существует, loaded/empty view остаётся на экране, а текущая локальная refresh-индикация и recovery flow продолжают работать рядом с ним. Skeleton выбирается только для состояния без доступного результата либо для отдельно незагруженного content block.
Так state choice остаётся детерминированным: наличие usable content имеет приоритет над skeleton. Альтернатива показывать skeleton поверх cached data отклонена, потому что она скрывает полезный контекст и смешивает initial loading с background refresh.
5. Проверяется наблюдаемая граница, а не разметка библиотеки
Risk-based UI coverage фиксирует выбор skeleton для initial/partial и auth/route loading, сохранение loaded context для refresh/pending states и отсутствие интерактивности у placeholders. Тесты не закрепляют внутренний DOM Mantine или точные декоративные размеры; layout fidelity подтверждается scoped presentation review на существующих responsive boundaries.
Delivery Map
| Area | Status | Architectural outcome or N/A reason |
|---|---|---|
| frontend | affected |
App, page и section presentation boundaries используют структурные skeletons для initial/partial loading, сохраняя contextual progress и текущие state machines. |
| backend | N/A |
Запросы, ответы, auth semantics, данные и server-side поведение не меняются. |
| specs | affected |
Новый capability contract frontend-loading-presentation остаётся canonical описанием loading invariants. |
| docs | affected |
После поставки as-built frontend и feature documentation отражает различие skeleton loading и contextual progress без копирования planning artifacts. |
Contract Gate Decision
| Gate | Status | Reason |
|---|---|---|
| runtime-openapi | N/A |
Изменяется только frontend presentation; ни backend API, ни frontend contract snapshot не меняются. |
Risks / Trade-offs
- Skeleton расходится с loaded layout. Page- и section-owned shapes следуют тем же semantic regions и responsive containers; декоративные точные размеры не превращаются в отдельный общий contract.
- Skeleton ошибочно заменяет полезный cached context. Существующая ViewModel
граница initial loading versus
isFetchingсохраняется, а contextual indicators остаются отдельным presentation path. - Auth bootstrap показывает неверный пользовательский flow. Neutral shell
не содержит profile или route outcome, а существующие guards остаются
единственным владельцем решения после
ready. - Декоративные placeholders создают шум assistive technology. Busy status назначается области целиком; внутренние shapes не интерактивны и не выдают fake content.
- Больше специализированных view components. Дополнительные небольшие views приняты как цена точного соответствия разным экранам; общий generic skeleton оставил бы меньше файлов, но больше условностей и visual drift.
Migration Plan
- Ввести app-level neutral shell/content fallbacks и согласовать общую busy-семантику на существующих Mantine primitives.
- Перевести top-level page loading views и partial section states на принадлежащие им skeleton shapes, не меняя ViewModel и data flow.
- Подключить route и auth-bootstrap fallbacks, сохранив текущие redirect и session transitions.
- Удалить устаревший generic spinner-loading path после отсутствия consumers; compact mutation и cached-refresh indicators оставить без изменений.
- После подтверждения поведения обновить durable as-built documentation.
Rollback Contract
| Field | Decision |
|---|---|
| Status | required |
| Reason | Auth decision не меняется, но новый neutral fallback отображается до завершения session bootstrap и поэтому должен иметь явно безопасный путь отката. |
| Trigger | Bootstrap fallback показывает public/protected route content до ready, нарушает redirect после определения сессии либо создаёт блокирующую accessibility regression. |
| Actions | Вернуть auth guards к прежнему blank fallback, сохранив существующие ready/user decisions; остальные page и section skeletons можно оставить независимо. |
| Data safety | Откат меняет только presentation; session tokens, query cache, backend data и persistence не преобразуются. |
| Verification | Подтвердить отсутствие route content до ready, прежние public/protected redirects после bootstrap и успешный frontend canonical gate. |