spec: archive loading skeleton change

This commit is contained in:
2026-08-27 21:43:57 +03:00
parent 2c9d163d39
commit 1d934e01d7
6 changed files with 405 additions and 0 deletions
@@ -0,0 +1,162 @@
## 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
1. Ввести app-level neutral shell/content fallbacks и согласовать общую
busy-семантику на существующих Mantine primitives.
2. Перевести top-level page loading views и partial section states на
принадлежащие им skeleton shapes, не меняя ViewModel и data flow.
3. Подключить route и auth-bootstrap fallbacks, сохранив текущие redirect и
session transitions.
4. Удалить устаревший generic spinner-loading path после отсутствия consumers;
compact mutation и cached-refresh indicators оставить без изменений.
5. После подтверждения поведения обновить 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. |