215 lines
20 KiB
Markdown
215 lines
20 KiB
Markdown
## 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 возвращает прежний вид без преобразования данных. |
|