Files
specs/openspec/changes/archive/2026-08-27-replace-loading-spinners-with-skeletons/design.md
T

12 KiB
Raw Blame History

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.