Files
specs/openspec/changes/account-top-positions/analysis/ux-design.md
T
codex-bot c66a5cd962
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
spec: record account-top-positions planning and implementation (WealthLedger/main#48) (#69)
2026-11-01 19:29:11 +03:00

36 KiB
Raw Blame History

UX Design

Visual Package: v2 (2026-10-30), локальные SVG в ../visuals/, хеши в разделе «Materials». v1 (четыре макета без центральной суммы, предупреждения и без разрезов с одной валютой) заменён. Базис существующих паттернов (код, не визуальные материалы): frontend @ 71126c91f0b30f4d65a313ddc7d7f86963340455, Mantine / @mantine/charts 9.7.3, docs/design/overview.md. Storybook не запускался, истории прочитаны как исходники.

Поведение определяют Delta Specs portfolio-concentration (28 Requirements, с учётом решений владельца #48, 3081) и portfolio-view. Документ ссылается на Requirements и не вводит нового поведения; всё, чего в Specs нет, вынесено в «Design-level items».

User Goals

Единственный пользователь — владелец, который отвечает на вопрос «где сконцентрированы мои деньги» и сравнивает вклад бумаг, секторов и эмитентов. Цель владельца: «сделать что-то классное и красивое» без выхода за пределы дизайн-системы.

Цель Что должно быть видно за один взгляд
Понять степень концентрации «Топ-5 · N %» и сумма разреза в центре кольца
Узнать, кто в топе Ранжированный список с названием, суммой, долей и длиной полосы
Не потерять остальное Строка «Прочее» с числом элементов и раскрытием
Доверять цифрам Одна валюта, предупреждение «Данные неполные», сноска «Не учтено позиций: N», одинаковые значения в кольце и списке

User Flows

Требования: portfolio-concentration — Вкладка «Концентрация» страницы счёта, Вкладка в состояниях страницы, Переключение разрезов, Начальное состояние вкладки, Одна валюта за раз, Доступные валюты вкладки, Раскрытие строки «Прочее», Строки без переходов, Сноска о не учтённых позициях, Предупреждение о неполных данных.

flowchart TD
    A[Страница счёта, вкладка «Позиции»] -->|Клик / Enter на «Концентрация»| B[Разрез «Акции», первая валюта по ISO]
    B --> C{Что делает User}
    C -->|Выбирает другой разрез| D[Разрез заменён, валюта сохранена, «Прочее» свёрнуто]
    C -->|Выбирает валюту, если валют две и больше| E[Пересчёт по позициям валюты, «Прочее» свёрнуто]
    C -->|Нажимает «Прочее»| F[Остальные элементы раскрыты, топ-5 и кольцо без изменений]
    F -->|Повторное нажатие| C
    C -->|Читает предупреждение и сноску| G[«Данные неполные» над разрезом; «Не учтено позиций: N» под списком]
    C -->|Переключает другую вкладку страницы| H[Разрез и валюта сохраняются]
    H -->|Возврат на «Концентрация»| C
    C -->|Уходит со страницы или переключает счёт| I[Состояние сброшено]
    I -->|Новый заход| B
    J[Режим «Все счета»] -.->|Вкладки нет| X((выход))
  1. Открыть вкладку. Вход: «Концентрация» в Tabs.List справа от «Позиции». Выход: разрез «Акции», первая валюта, кольцо с центром, список.
  2. Переключить разрез. Один шаг на SegmentedControl. Валюта остаётся, «Прочее» сворачивается.
  3. Переключить валюту. Select; пересчитывается текущий разрез, «Прочее» сворачивается. При одной валюте выбора нет, шаг пропускается.
  4. Раскрыть «Прочее». Нажатие на строку (мышь, касание, Enter/Space), повторное нажатие сворачивает.
  5. Прочитать предупреждение и сноску. Оба — текст без действий.
  6. Вернуться с другой вкладки страницы. Разрез и валюта прежние; новый заход (уход со страницы, смена счёта, перезагрузка) — снова «Акции» и первая валюта.
  7. Пустой разрез. User остаётся на вкладке и переключает разрез или валюту (если выбор показан).

Information Architecture and Screen Structure

Положение: Tabs.Tab value="concentration" между «Позиции» и «Деньги». Вкладка не меняет defaultValue (positions); панель с keepMounted={false}, как у остальных. Требование сохранять разрез и валюту при переключении вкладок означает, что выбор живёт выше панели; способ — в design.md.

Порядок внутри панели:

  1. Предупреждение «Данные неполные» — только при неполноте Portfolio, над карточкой (M5, M6). Один блок для всех разрезов: остаётся на месте при переключении разреза.
  2. Карточка (Paper withBorder radius="lg"): шапка, затем две колонки на десктопе / одна на мобильном.
  3. Шапка. SegmentedControl «Акции / Облигации / Секторы / Эмитенты» (aria-label «Разрез концентрации»). Справа подпись «Валюта» и Select — только если валют две и больше; иначе шапка содержит один переключатель (M5, M6).
  4. Hero-блок (подложка blue.0, градиент в белый, радиус lg): кольцо 220 px, толщина 32. Центр: вверху «Топ-5 · N %» (muted, только если элементов шесть и больше), под ним сумма разреза (bold). Без метрики — только сумма (M6). Под кольцом подпись «<Разрез> · <валюта>».
  5. Ранжированный список (<ol>): номер, маркер, [логотип], название и подпись, справа сумма и доля, полоса доли.
  6. Строка «Прочее» (при семи и более элементах): кнопка-disclosure с «N элементов», суммой, долей и шевроном. Раскрытие показывает остальные элементы вложенным списком (M2).
  7. Сноска «Не учтено позиций: N» (только при N > 0).

Содержимое строки по разрезам:

Разрез Логотип Подпись под названием
Акции PositionLogo 40 px, без ссылки тикер или идентификатор позиции
Облигации PositionLogo 40 px, без ссылки ISIN
Секторы нет нет
Эмитенты нет (Строки без переходов) нет

Логотип в «Секторах» не нужен: элемент объединяет позиции. Строка «Прочее» логотипа не имеет. Ни одна строка не ведёт на другую страницу; PositionLogo и название не получают to.

Кодирование без цвета (Доступность диаграммы без цвета, Группы «Сектор не указан» и «Эмитент не указан»):

Признак Кодирование
Ранг 1–6 Цифра в строке и у сегмента (для сегментов ≥ 4 %) + монохромная синяя шкала blue.9 → blue.0 с обводкой blue.9
«Прочее» Серая точечная штриховка в маркере, сегменте и полосе, серая обводка; слово «Прочее»
«Сектор не указан» / «Эмитент не указан» Диагональная штриховка поверх цвета ранга; название в тексте
Значение Всегда число в тексте строки; полоса и кольцо его дублируют

Шкала однотонная, потому что grape, orange и green в «Структуре портфеля» означают классы активов (portfolioCategoryPresentation). Длина полосы — доля от 100 % суммы разреза, шкала совпадает с кольцом.

Центр кольца. Внутренний диаметр 156 px. «Топ-5 · N %» — 14 px / 600, сумма — 22 px / 700; сумма точная, без округления (Центр диаграммы), поэтому при длинной записи размер ступенчато уменьшается до 14 px (22 → 18 → 14), без переноса и обрезки. Обе строки входят в текстовый эквивалент.

Interaction Behavior

Элемент Поведение
Переключение разреза Fade + translateY(4px) 220 ms ease-out, как смена валюты в PortfolioStructure; при prefers-reduced-motion: reduce анимации нет. Вращения сегментов нет (isAnimationActive: false). Фокус остаётся на переключателе.
Смена валюты Та же анимация; «Прочее» сворачивается. Если выбранная валюта пропала после обновления Portfolio, выбирается первая (Независимый выбор валюты вкладки), без уведомления. Если валюта осталась одна, Select исчезает.
Строка «Прочее» <button> с aria-expanded и aria-controls; зона — вся строка, высота ≥ 44 px. Шеврон поворачивается на 180°, список раскрывается ≤ 200 ms (при reduce-motion — мгновенно). Фокус остаётся на кнопке. Пять строк и кольцо не меняются.
Строки топ-5 и раскрытые элементы Не интерактивны: курсор обычный, переходов нет (Строки без переходов).
Hover/focus Hover на сегменте подсвечивает строку и показывает tooltip «название · сумма · доля» (вид PortfolioTooltip); hover на строке подсвечивает сегмент. Это подсветка, не действие; вся информация доступна в тексте.
Касание Tooltip по касанию сегмента — дополнение; список читается без жестов.
Фокус Стандартное кольцо Mantine, не скрывается (показан на «Прочее» в M2).
Предупреждение «Данные неполные» Статический блок Alert role="status", без кнопок; ручного обновления нет (portfolio-view).

UI States

Данные вкладки выводятся из уже загруженного Portfolio; собственных loading и error у вкладки нет. Состояния — только те, что есть в Specs: Вкладка в состояниях страницы, Пустое состояние разреза, Предупреждение о неполных данных, Сноска о не учтённых позициях, Доступные валюты вкладки, Состояния Portfolio на вкладке «Концентрация».

Экран / область Loading Empty Error Success Partial / stale
Страница счёта (Portfolio) Skeleton страницы (AccountPageLoading); вкладки «Концентрация» нет Штатный пустой Portfolio: вкладки нет (AccountPageEmpty) StateView error (AccountPageError); вкладки нет — Stale: SourceSyncStatus над вкладками, как для «Позиций»; разрезы по сохранённому снимку. Partial: вкладка есть, над разрезом «Данные неполные» (M5, M6), разрезы по доступным данным
Вкладка, разрез с данными n/a n/a n/a Центр, кольцо, список, сноска при N > 0 (M1–M3, M5, M6) Сноска «Не учтено позиций: N». Позиции без оценки при полном Portfolio предупреждение «Данные неполные» не вызывают
Разрез без учитываемых элементов (в том числе у счёта только с денежными остатками) n/a Название разреза и валюта (если валюта есть) вместо кольца, списка, метрики; Select (если показывается) и сноска остаются (M4) n/a n/a Сноска при N > 0
Выбор валюты n/a Скрыт при одной валюте и при отсутствии валют n/a Select при двух и более —
Метрика «Топ-5 · N %» n/a Скрыта при пяти и менее элементах (M6) n/a Показана при шести и более (M1, M2, M5) —
«Прочее» n/a Строки нет при шести и менее элементах (M5) n/a Свёрнуто / раскрыто —
Группы «не указан» n/a n/a n/a Обычный элемент с названием и штриховкой (M2, M5) Не влияют на успешность и свежесть Portfolio

Disabled, offline, «нет прав»: не применимо (действий записи нет); недоступный счёт обрабатывает страница, вкладка в этих состояниях не показывается.

Validation Feedback

Пользовательского ввода, кроме выбора из закрытых списков (разрез, валюта), нет; валидации и ошибок ввода нет. Обратная связь на выбор — мгновенная перерисовка (см. Design-level items про озвучивание).

Responsive Behavior

Опора на существующую границу 48em, уже применяемую в PortfolioStructure.module.css.

Ширина Раскладка
≥ 48em, макеты M1, M2, M5 (1280) Шапка в одну строку. Две колонки: hero 296 px + список, gap 32 (xl). Предупреждение (если есть) на всю ширину над карточкой.
< 48em, макеты M3, M4, M6 (390) Одна колонка: переключатель разрезов во всю ширину; при двух и более валютах ниже строка «Валюта + Select» (в M6 её нет); hero во всю ширину, кольцо 220 по центру; ниже список. Строка: логотип 40 (если положен), название и подпись слева, сумма и доля справа, полоса под ними.
320 px (минимум) Переключатель разрезов — в контейнере с overflow-x: auto (паттерн positionFilter), подписи не сжимаются; страница не скроллится по горизонтали.
Вкладки страницы на мобильном Существующая Tabs.List (flex-wrap: nowrap; overflow-x: auto, класс tabsList) прокручивается по горизонтали. В макетах (M3, M4, M6) это показано так: вкладка «Операции» обрезана краем экрана, а не вмещена целиком, и справа 40 px градиентное затухание. Обрезанная вкладка — основной признак прокрутки; затухание — необязательное усиление (см. Design-level items). «Концентрация» — вторая вкладка и на 320 px видна целиком.
Длинные названия Название переносится до двух строк; суммы white-space: nowrap, font-variant-numeric: tabular-nums. Отсутствие названия — fallback из Названия элементов.
Много элементов в «Прочее» Вложенный список без собственной прокрутки.
Масштаб текста 200 % Колонки схлопываются в одну; числа не усекаются; сумма в центре кольца уменьшается ступенчато, но не обрезается.

Целевые зоны переключателя, Select и «Прочее» ≥ 40–44 px (WCAG 2.2 AA 2.5.8: ≥ 24 px).

Accessibility

Ориентир: WCAG 2.2 AA. Основа — Доступность диаграммы без цвета и Центр диаграммы.

Элемент Роль / имя
Панель Tabs.Panel, имя от вкладки; внутри <section aria-labelledby> с заголовком «Концентрация» (visually hidden или Title order={2})
Предупреждение Alert role="status" с текстом «Данные неполные», не alert (не прерывает)
Переключатель разрезов SegmentedControl (radiogroup), aria-label «Разрез концентрации»
Валюта Select, aria-label «Валюта концентрации» (отличается от «Валюта структуры портфеля»); при одной валюте элемента нет в DOM
Кольцо role="img", имя «Распределение: <разрез>, <валюта>» + aria-describedby на скрытый текст: центр («Топ-5 · 86,5 %, 2 235 310 ₽» либо только сумма) и затем «1. Сбербанк, 612 480 ₽, 27,4 %; … Прочее, …» в порядке списка. Сегменты в порядок фокуса не входят
Список <ol aria-label="<разрез>: топ элементов">; каждый элемент читается как «название, сумма, доля»
Полоса, маркер, штриховка aria-hidden (дублируют текст)
«Прочее» <button aria-expanded aria-controls>, имя «Прочее, N элементов, <сумма>, <доля>»
Сноска Обычный текст, не alert

Клавиатура. Порядок фокуса: вкладки страницы → разрез → валюта (если показана) → «Прочее» (если есть) → остальной контент. Строки не фокусируются, потому что на них нет действий. Enter/Space на «Прочее» переключает раскрытие; фокус при раскрытии не уходит. Прокручиваемый ряд вкладок на мобильном остаётся доступен с клавиатуры стрелками (поведение Tabs).

Не только цвет. Ранг — цифра + светлота + обводка; «Прочее» — точечная штриховка + слово; «не указан» — диагональная штриховка + слово; предупреждение — текст «Данные неполные».

Контраст (расчёт по токенам @wealthledger/tokens, светлая схема)

Пара Коэффициент Требование
text dark.7 на белом 15,5 : 1 4,5
text dark.7 на blue.0 hero 14,3 : 1 4,5
muted gray.7 на белом / на blue.0 hero 8,2 : 1 / 7,5 : 1 4,5
Обводка сегмента blue.9 на белом 10,4 : 1 3 (граница графики)
Заливка blue.6 полосы на треке gray.1 4,7 : 1 3
Серый контур «Прочее» gray.6 на белом 3,3 : 1 3
Светлые заливки рангов 4–6 (blue.3/1/0) на белом 1,8 / 1,2 / 1,1 : 1 Информации не несут: различимы обводкой blue.9 и цифрой
gray.6 для мелкого текста 3,3 : 1 Не использовать; muted в теме — gray.7

Тёмная тема. docs/design/overview.md: «тёмная тема не заявлена», токены описывают только светлую схему; контраст для тёмной не рассчитывался. Цвета в реализации берутся из токенов / CSS-переменных; литералы в SVG нужны только для самодостаточности.

Движение. prefers-reduced-motion: reduce отключает fade, раскрытие и переходы подсветки.

Увеличение текста. До 200 % без потери информации.

Design System Reuse

Часть Решение Обоснование
Вкладка Reuse Tabs.Tab страницы счёта Консистентная навигация
Прокрутка вкладок на мобильном Reuse tabsList (overflow-x: auto) Уже работает; затухание у правого края в макете — необязательное улучшение
Переключатель разрезов Reuse SegmentedControl + обёртка с overflow-x, как у «Акции / Облигации / Прочее» Тот же паттерн внутри вкладки
Выбор валюты Reuse Select (allowDeselect={false}, w=112, aria-label) из PortfolioStructure; скрывается при ≤ 1 валюте, как там Единое поведение и вид
Кольцо Reuse DonutChart (size 220, thickness 32, startAngle 90 / endAngle -270, без анимации, tooltip по сегменту) Знакомая форма
Tooltip Reuse PortfolioTooltip (Paper, Text fw=600) Тот же вид
Логотипы Reuse PositionLogo (Avatar 40, radius md, fallback — первая буква) без to Совпадает с PositionCard; переходов по Specs нет
Предупреждение Reuse Alert role="status" (как investment.partial страницы) Тот же вид неполных данных
Состояния страницы Reuse StateView, SourceSyncStatus, LoadingSkeleton Наследуются без изменений
Анимация смены Reuse keyframes currency-change Единый ритм
Центр кольца из двух строк Новый («Топ-5 · N %» + сумма вместо одной chartLabel) Требует Центр диаграммы; реализация — в design.md
Строка рейтинга с полосой Новый (номер, маркер, название, сумма/доля, полоса) Нет компонента «строка доли»; EntryCard и PositionCard выражают карточку операции/позиции. Собирается из Group, Text и полосы
Шкала рангов Новый (монохромные blue.9…0) Не смешивать с цветами классов активов; только палитра токенов
Штриховки Новый вспомогательный признак Доступность диаграммы без цвета; название остаётся главным признаком
Disclosure «Прочее» Новый по структуре (UnstyledButton + Collapse) В @wealthledger/ui аналога нет; поведение задано Specs
Видимая подпись «Валюта» рядом с Select Отклонение В PortfolioStructure только aria-label; подпись поясняет значение на мобильном, aria-label сохраняется. Можно снять без последствий для Specs
Подпись «<Разрез> · <валюта>» под кольцом Новый текст Помогает при одной валюте, когда Select скрыт; значений не добавляет

Новые токены не вводятся; в @wealthledger/ui ничего не добавляется, предметные компоненты остаются в приложении (docs/design/overview.md).

Materials

Относительные пути заданы от каталога этого файла. SVG самодостаточны (без scripts, handlers, внешних шрифтов и ресурсов; font-family: system-ui, sans-serif), содержат <title> и <desc> с текстовым эквивалентом, включая центр кольца. Данные синтетические; цвета в SVG — литералы токенов для самодостаточности.

PNG-превью не создавались: единственный доступный рендерер (qlmanage) выдаёт квадратные миниатюры с полями, непригодные как эталон.

ID материала Относительный путь / ссылка Экран/состояние и назначение SHA-256
M1 ../visuals/desktop-stocks.svg Desktop 1280: «Акции», RUB, две валюты (выбор виден), 8 элементов: центр «Топ-5 · 86,5 %» + сумма, топ-5 с логотипами, «Прочее» свёрнуто, сноска «Не учтено позиций: 2» c4ef8f814f6b168d3d96b6b8288ee6c574b4cf361de451a2152b1e51c939dae5
M2 ../visuals/desktop-sectors-expanded.svg Desktop 1280: «Секторы», «Прочее» раскрыто и в фокусе, «Сектор не указан» со штриховкой на 5-м месте, без логотипов, центр «Топ-5 · 92,1 %» + сумма b59256bbcca8a414af85b487966dbda314ca30bbe20c5a65cf313096b4305106
M3 ../visuals/mobile-stocks.svg Mobile 390: «Акции», одна колонка, выбор валюты в отдельной строке, прокручиваемый ряд вкладок (обрезанная «Операции» + затухание) d0ed332a78bdf8c9646a3033db63fc45643b4f365b0fabf34f2433b815663dbd
M4 ../visuals/mobile-empty-slice.svg Mobile 390: пустое состояние «Облигации» в RUB (валют две, выбор виден), сноска «Не учтено позиций: 1» 04f8be877948c3a46f0cea64558c5da1a3577a3a4b947f32b7341f83eda0a377
M5 ../visuals/desktop-issuers-warning.svg Desktop 1280: «Эмитенты», одна валюта (выбора нет), предупреждение «Данные неполные», шесть элементов без «Прочее», метрика «Топ-5 · 94,2 %» + сумма, без логотипов, «Эмитент не указан» со штриховкой ece4fa5499cd5c326569e3a3b3069546dc66964b9972cac65140449206fcb4ae
M6 ../visuals/mobile-issuers-warning.svg Mobile 390: «Эмитенты», пять элементов: в центре только сумма (метрики нет), предупреждение «Данные неполные», без выбора валюты и без логотипов 1380c45d3db93c9b778a77c210f4444a19fe6666dff8b32080312584b78719ae

Что проверено: валидный XML (xmllint), отсутствие scripts, href, <image> и @font-face, оценка наложений текста, расчёт контраста по токенам. Макеты не просматривались на отрисованном изображении. Единственное «выходит за экран» по оценке — «Операции» в M3, M4, M6, и это намеренная обрезка clipPath. Макеты — иллюстрация компоновки, а не пиксельная спецификация.

Закрытые findings для Specs

Решения владельца (#48, 3081) внесены в Specs; UX-часть приведена в соответствие.

ID Решение Requirement Где отражено
S1 Разрез и валюта сохраняются, пока User на странице, включая переключение вкладок; сбрасываются при новом заходе Начальное состояние вкладки User Flows 6, IA
S2 Select скрыт при одной валюте и без валют Доступные валюты вкладки IA 3, UI States, M5, M6
S3 Метрика «доля топ-5» скрыта при пяти и менее элементах Суммарная доля топ-5 IA 4, UI States, M6
S4 Неполный Portfolio: вкладка есть, над разрезом «Данные неполные» Предупреждение о неполных данных IA 1, UI States, M5, M6
S5 Вкладки нет при загрузке, ошибке и штатном пустом Portfolio Вкладка в состояниях страницы UI States
S6 Неизвестный сектор показывается как получено, без пояснений Подписи секторов Без UX-обработки; текст читается скринридером как есть
S7 «Прочее» показывает число остальных: «2 элемента», «5 элементов» Топ-5 и строка «Прочее» IA 6, M1, M2
S8 (часть) Центр кольца: «Топ-5 · N %» и сумма разреза Центр диаграммы IA 4, Accessibility
S9 Без логотипов в «Эмитентах» и без переходов из строк Строки без переходов IA, Interaction Behavior, Design System Reuse

Ранее предложенные вопросы Q1 (сумма разреза), Q3 (логотип эмитента), Q4 (переход из строки) закрыты теми же Requirements; Q8 (масштаб полосы от 100 %) принят как дизайн-решение.

Design-level items

Поведения не меняют; остаются на уровне дизайна, подтверждаются в design.md / на ревью макетов. Пункты D1–D4 перенесены из прежних вопросов по просьбе владельца, D5–D7 найдены при обновлении.

ID Вопрос Предложение по умолчанию
D1 Тексты пустого состояния. Specs задают содержание (название разреза и валюта, если есть), но не формулировку Заголовок «<Разрез> в <валюта>: нет позиций» (без валюты, если валют нет); пояснение «Нет позиций с положительной оценкой в этом разрезе и выбранной валюте.»
D2 Озвучивание смены разреза и валюты (бывший S8) Одна role="status" (polite) строка: «Разрез «Секторы», RUB: 8 элементов»; при первом показе не объявляется
D3 Минимальная видимая дуга для очень малых долей Минимума нет (доли честные); сегмент опознаётся по списку и обводке
D4 Нумерация внутри раскрытого «Прочее» Продолжение ранга (6, 7, …), как в M2
D5 Сообщения о неполноте: на вкладке «Концентрация» страничное «Период загружен не полностью» не показывается, остаётся только «Данные неполные» на вкладке Закрыт: PC «Предупреждение о неполных данных», решение владельца 3082; реализация — design.md F4
D6 Затухание у правого края мобильных вкладок отсутствует в коде; ряд уже прокручивается и обрезанная вкладка сама показывает прокрутку Затухание не обязательно; если принимается, оно общее для всех вкладок страницы, а не только для «Концентрации»
D7 Реализация двухстрочного центра DonutChart (children либо наложенный Text) и ступенчатое уменьшение суммы Решает design.md; UX требует две строки, точную сумму без обрезки и текстовый эквивалент