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

215 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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](proposal.md) и
[Delta Specs](specs/portfolio-view/spec.md). 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](visuals/a1-wide.png) | 1440, три обычных итога | `b406552e01098a5b95183d6a54bd59b3bf827387d33d73a6a453d83140b8da10` |
| narrow (j6ks4) | [visuals/a1-narrow.png](visuals/a1-narrow.png) | 390, три обычных итога | `8ee71bb406abaa980e08f57c45e007fb66183786dfd251c9280a6ff62205d922` |
| single (iwyJA) | [visuals/a1-single-wide.png](visuals/a1-single-wide.png) | 1440, одна валюта | `6f163dd4a4ba461df5e715bab5757f4698986aade56ae38f02585d9c8d366b28` |
| long-wide (Hbu1s) | [visuals/a1-long-wide.png](visuals/a1-long-wide.png) | 1440, длинные значения | `ecccaee124c72b0f4f22bd92b31ef97e260c830782761ed9e7aab4c33a7e0af0` |
| long-320 (vjlkn) | [visuals/a1-long-320.png](visuals/a1-long-320.png) | 320, перенос и сохранение всех цифр | `48d62eaa497819e6312f5ae56afa393f92c7bb0970543cd9b3abdd5960480534` |
| loading (DuAlU) | [visuals/a1-loading-narrow.png](visuals/a1-loading-narrow.png) | 390, placeholders сводки | `138b000b334f54294f6cd8e88d607619608b05620231267fc1dc0477e1f87f46` |
| refresh (aP2cU) | [visuals/a1-refresh-narrow.png](visuals/a1-refresh-narrow.png) | 390, сохранение данных при обновлении | `e453606105a96290a2d2358e039935c89187a90127ffe4228374ae6e16c9e252` |
| error-stale (z5EeB) | [visuals/a1-error-stale-narrow.png](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 возвращает прежний вид без преобразования данных. |