Files
specs/openspec/changes/compact-portfolio-totals/design.md
T

20 KiB
Raw Blame History

Context

Страница Brokerage Account уже разделяет получение данных в AccountPage.vm.ts и отображение в AccountPageLoaded.view.tsx. PortfolioTotals получает готовые totalsByCurrency; текущая сводка использует Mantine SimpleGrid и Paper, вертикальную метку ISO над суммой, отступ 16 px и верхнюю синюю границу 3 px. Максимальная ширина контента страницы — 960 px.

shared/lib/display.ts уже форматирует decimal string без числового преобразования: сохраняет знак и дробные позиции, группирует целую часть. formatMoney меняет только RUB на ₽. Query и VM уже сохраняют успешные данные при refetch/error; PortfolioRefreshState отделён от server-side isStale. Действующий initial skeleton содержит общий блок высотой 120 px вместо геометрии валютных карточек.

Основание подхода — утверждённые Proposal и Delta Specs. A1 из Discovery выбран владельцем; ни выбор, ни прежние PNG сами по себе не были approval Design.

Goals / Non-Goals

Goals: изменить представление сводки и её loading placeholder в существующем page slice; сохранить строковую точность и валютную идентичность; использовать нынешние primitives, theme tokens и границы VM/view.

Non-Goals: менять финансовые вычисления, транспортные типы, query/cache policy, state machine, общие правила денежных суффиксов приложения или соседние разделы страницы. Новые зависимости, инструментальные витрины и сервисы форматирования не нужны.

Decisions

Ownership и поток данных

PortfolioTotals остаётся владельцем компактного представления каждого переданного итога. Порядок, membership и ключи берутся из исходных валют; никакой повторной агрегации, сортировки по знакам или дополнения валют нет. VM продолжает выбирать loaded/empty по составу Portfolio, а не по итогам. Пустой список итогов не создаёт карточек или отдельного empty-состояния.

Loaded view сохраняет своё место сводки после времени снимка и перед составом. Его timestamp, refresh/error и stale sections продолжают получать прежние props. Меняется только сводка и соответствующая область page-owned skeleton. Это удерживает ответственность за устойчивость данных в query/VM, а за геометрию — в presentation.

Точное форматирование и валютная идентичность

Переиспользовать существующий formatDecimal, открыв его через публичный shared/lib export без изменения алгоритма и поведения formatMoney. Выбор метки и суффикса принадлежит сводке: RUB → ₽, USD → $, EUR → €; для остальных валют метка и суффикс — исходный ISO-код. Например, USD/CAD дают $/CAD, поэтому совпадающие мировые знаки не требуют каталога валют или анализа всей коллекции. Неизвестные валюты не отбрасываются.

Полные названия трёх валют принадлежат существующему русскому i18n-каталогу. Семантика карточки — пара название/значение (dl, dt, dd либо эквивалентная связь): доступное название содержит «Российский рубль», «Доллар США» или «Евро»; для прочих валют доступен ISO-код. Декоративный badge скрыт от accessibility tree, полное текстовое значение остаётся доступным. Карточка не кнопка, не получает tab stop и не требует tooltip/hover для определения валюты.

Числа не проходят через Number, parseFloat, округление или currency-mode Intl.NumberFormat. Пробелы группировки и десятичная запятая сохраняются; перенос — CSS layout, а не вставка символов в числовые данные. -0.5 остаётся -0,5, исходные дробные нули сохраняются. Замена ISO-кода в уже собранной строке formatMoney отвергнута: она связывала бы сводку с деталями суффикса общего formatter. Новый дубликат decimal formatter также не нужен.

Компоновка средствами платформы

Mantine Paper и page-local CSS остаются основой карточек. Сетка сохраняет колонки base: 1, sm: 2, md: 3 (48 em / 62 em), с gap 10 px. Одна валюта занимает одну ячейку сетки, как в A1; дополнительные валюты заполняют следующие строки, без фиктивных пустых карточек.

Внутри карточки метка и сумма образуют переносимую строку. Естественная ширина полного текста определяет вместимость, без JS-измерений и порогов по количеству цифр. При недостатке места сумма переходит ниже метки и получает всю доступную ширину. Ограничения min-width: 0, max-width: 100%, обычные переносы на пробелах и overflow-wrap: anywhere защищают также длинную дробную часть. Не задаются фиксированная высота, nowrap, ellipsis, clipping или уменьшение шрифта ради вместимости. На переносимой строке знак минуса не отделяется от первой цифры; остальные цифры и суффикс остаются текстом в исходном порядке.

Альтернативы: единая общая полоса не соответствует выбранным отдельным карточкам; фиксированная двухстрочная раскладка теряет компактность обычных значений; обрезание/скролл/округление нарушают Delta Specs. Нативный reflow сохраняет простую архитектуру и работает при увеличении текста.

UI/UX Applicability and Package

Applicability and scope: new/changed — сводка и её initial skeleton на /accounts/:accountId. Навигация, heading, время, позиции, Cash Balance и остальные секции на PNG служат контекстом; их внешний вид не является заданием на редизайн. Наличие в runtime дополнительных секций не является расхождением.

Package revision: a1-symbols-r1. Один approval относится ко всему этому Markdown и восьми локальным PNG ниже. Они доступны без Pencil, Storybook, исходного чата или внешнего аккаунта.

Reused source and differences: это новый canonical пакет, а не ссылка на approved archived reuse. PNG перенесены byte-for-byte из выбранных Discovery r3 exports; их первоначальные имена перечислены ниже. Исходный .pen не является зависимостью пакета. Markdown дополняет изображения правилами для других валют, accessibility, промежуточных ширин и произвольно длинных значений.

Визуальная геометрия

Карточка белая, граница gray-3 1 px со всех сторон, radius 8 px, без отдельной синей верхней полосы и тени. Padding 12 px; промежуток метка/сумма 10 px. Слева badge: фон blue-0, текст blue-6, radius 4 px, минимум 40 × 24 px; для ISO-кодов ширина растёт по тексту. Знак 18 px / 600. Сумма — 20 px / 600, line-height 1.55, основной цвет dark-7. Используется системный font stack приложения, без загрузки Roboto только ради совпадения canvas.

Обычная строка выравнивает сумму к правому краю. После перехода под метку сумма использует всю ширину с чтением слева направо. Предпочтительны переносы на пробелах группировки; длинная непрерывная дробь может переноситься между цифрами. Точная точка переноса зависит от системного шрифта: PNG не задаёт обязательный разрыв после конкретной цифры. Все цифры, минус и суффикс читаемы.

При базовом масштабе обычная карточка ориентировочно 57 px, три карточки на mobile — 191 px вместе с двумя gap. Это design geometry, а не runtime замер: критерием служат полные значения и уменьшение высоты относительно прежнего UI при одинаковых ширине, шрифте, масштабе и данных. Длинные значения и увеличение текста закономерно увеличивают высоту.

Screens, states and interactions

Состояние Решение
Loaded, 1/3 валюты Отдельные A1 карточки; символ слева, полная сумма справа при вместимости.
Loaded, больше валют / неизвестная валюта Та же сетка без лимита количества; прочие валюты используют ISO-код в badge и суффиксе.
Длинные / отрицательные значения Сумма уходит под badge и переносится; -0,5 $ сохраняет минус; зелёный/красный тон по знаку не добавляется.
Initial loading Три декоративных card placeholders с badge/amount shapes, в той же responsive сетке; число shapes не обещает три валюты. Существующий LoadingSkeleton владеет busy label, shapes скрыты от чтения и не фокусируются. Остальные semantic regions loading view сохраняются.
Refetch Прежние карточки и lastSuccessfulAt плюс существующий compact progress; карточки не заменяются skeleton.
Client refresh error Прежние значения и время плюс существующий orange warning и «Повторить».
Server stale Прежний blue warning и время; само isStale не создаёт retry.
Error + stale Оба самостоятельных предупреждения, карточки и время; retry принадлежит клиентской ошибке.
Retry → success До ответа сохраняются значения; успешный ответ обновляет значения/время, снимает client error; stale зависит только от нового ответа.
Нет итогов при непустом составе Нет карточек; состав и время остаются.
Пустые позиции и Cash Balance Существующее empty-представление, без нулевых карточек.
Ошибка без успешных данных Существующая error view; 404 ведёт к списку счетов без retry; ошибки подключения/токена сохраняют свои действия, временная/неизвестная ошибка допускает retry.

Новых взаимодействий нет. Кнопка повтора сохраняет клавиатурную доступность и существующую callback-связь с VM. Статические суммы не превращаются в live region; обновление сообщает существующий status region.

Responsive behavior: на 320/390 px одна карточка в ряду; при 1440 px и контенте 960 px — три. Между ними действуют указанные breakpoints. При любом числе колонок переполнение значения переводит его ниже метки, а не расширяет grid track. Одна валюта на узком экране занимает всю ширину. Loading использует те же правила; refetch/error/stale не меняют геометрию самих итогов.

Materials

Material ID (Discovery source) Relative path Screen/state and purpose SHA-256
wide (ACV5F) visuals/a1-wide.png 1440, три обычных итога b406552e01098a5b95183d6a54bd59b3bf827387d33d73a6a453d83140b8da10
narrow (j6ks4) visuals/a1-narrow.png 390, три обычных итога 8ee71bb406abaa980e08f57c45e007fb66183786dfd251c9280a6ff62205d922
single (iwyJA) visuals/a1-single-wide.png 1440, одна валюта 6f163dd4a4ba461df5e715bab5757f4698986aade56ae38f02585d9c8d366b28
long-wide (Hbu1s) visuals/a1-long-wide.png 1440, длинные значения ecccaee124c72b0f4f22bd92b31ef97e260c830782761ed9e7aab4c33a7e0af0
long-320 (vjlkn) visuals/a1-long-320.png 320, перенос и сохранение всех цифр 48d62eaa497819e6312f5ae56afa393f92c7bb0970543cd9b3abdd5960480534
loading (DuAlU) visuals/a1-loading-narrow.png 390, placeholders сводки 138b000b334f54294f6cd8e88d607619608b05620231267fc1dc0477e1f87f46
refresh (aP2cU) visuals/a1-refresh-narrow.png 390, сохранение данных при обновлении e453606105a96290a2d2358e039935c89187a90127ffe4228374ae6e16c9e252
error-stale (z5EeB) visuals/a1-error-stale-narrow.png 390, отдельные предупреждения и retry c63c8a30cc932582f591422dd4871c6663cb55567218cee988d7119d77d4a4e3

Delivery Map

Area Status Architectural outcome or N/A reason
frontend affected A1 totals и соответствующий skeleton; точное форматирование с локальными знаками и доступной идентичностью; сохранённые query/VM состояния.
backend N/A Готовые итоги, расчёты, Snapshot и HTTP/OpenAPI contract не меняются; серверная реализация не требуется.
specs affected Canonical planning, переносимый Design package и нормативное описание portfolio-view в рамках lifecycle.
docs affected После реализации features/portfolio-view.md описывает фактическую компактную сводку, валютные знаки и ограничения оценки.

Contract Gate Decision

Gate Status Reason
runtime-openapi N/A Ни одна сторона API contract не меняется; существующие decimal strings и исходные currency остаются входом представления.

Risks / Trade-offs

  • Разные системные шрифты и zoom меняют вместимость. Нужен browser reflow review обычных и длинных fixtures на узких и широких экранах; PNG не доказывает отсутствие runtime overflow или доступность. Точность и полнота важнее высоты.
  • Открытие существующего decimal formatter немного расширяет публичный shared/lib, зато исключает дублирование денежных преобразований. Риск защищается сохранением текущего formatMoney и regression cases для точных цифр, дробных нулей, -0.5 и валютной идентичности на presentation boundary.
  • Значение $ без доступного полного названия неоднозначно; связь пары название/значение проверяется в accessibility tree. Цвет не несёт идентичность.
  • Placeholder не знает количества валют заранее. Три формы повторяют ожидаемую структуру, а успешный ответ определяет реальное число карточек.
  • Геометрия и денежное представление требуют scoped acceptance проверки; состояние refetch/error проверяется через существующие account-page tests, без новой state machine или отдельной инфраструктуры тестирования.

Migration Plan

После approval Design и затем Tasks представление и loading geometry поставляются совместно с проверкой точности и сохранённых состояний. После проверенной реализации docs фиксирует as-built поведение; canonical спецификация синхронизируется в рамках штатного lifecycle. Миграции данных, переключатель функции и координированный релиз backend не нужны.

Rollback Contract

Field Decision
Status N/A
Reason Изменение обратимого frontend presentation без изменения auth, данных, API или другого труднообратимого решения. Обычный откат frontend delivery возвращает прежний вид без преобразования данных.