feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
+90
-89
@@ -1,104 +1,105 @@
|
||||
# Визуальный язык (фаза 6, начата)
|
||||
# Визуальный язык
|
||||
|
||||
Редизайн Flutter-клиента в духе Grimmory (self-hosted книжная библиотека): тёмный сине-
|
||||
индиго фон вместо нейтрального серого, плоские карточки с рамкой вместо тени, группированная
|
||||
боковая навигация с капс-заголовками и левой акцентной полоской у активного пункта. Обкатано
|
||||
целиком на одном экране — Обзор (`features/home/home_page.dart`) — и на навигационном шелле
|
||||
(он общий для всех экранов, поэтому не в счёт «раскатки»), прежде чем идти дальше, тем же
|
||||
способом, что офлайн-кэш (`offline-cache.md`).
|
||||
Клиент оформлен по образцу Snowball (сервис учёта инвестиций): графитово-фиолетовые поверхности,
|
||||
один яркий голубой акцент, плоские скруглённые карточки без рамки, заливные поля ввода,
|
||||
навигация вкладками сверху. Раньше был другой язык (сине-индиго, боковой сайдбар по референсу
|
||||
Grimmory) — он заменён целиком, кроме доменных экранов, которые сохранили свою раскладку и просто
|
||||
подхватили новые токены.
|
||||
|
||||
## Решение: одна тема на обе яркости, не «тёмная как есть — светлая как получится»
|
||||
|
||||
Тёмная и светлая тема получают один и тот же язык (форма карточек, акцент, поведение
|
||||
навигации), различаются только тона поверхностей. Причины:
|
||||
|
||||
- Приложение живёт на вебе, Linux, Windows и Android одновременно — пользователь может
|
||||
оказаться в любой теме по системной настройке, и она уже переключается в
|
||||
`settings_page.dart`. Бесплатно смотрящаяся только тёмная версия читалась бы как
|
||||
недоделанная в светлой.
|
||||
- Семантика цвета (`signColor` — прибыль/убыток, `ChartColors` — ряды графиков) уже общая
|
||||
для обеих тем; заводить для тёмной темы отдельный «фирменный» акцент, а для светлой
|
||||
оставлять дефолтный Material-синий было бы второй, несогласованной системой токенов.
|
||||
Одна тема на обе яркости: форма карточек, акцент и поведение навигации одинаковы, различаются
|
||||
только тона поверхностей. Приложение живёт на вебе, Linux, Windows и Android, и тема
|
||||
переключается в Настройках (`themeModeProvider`) — светлая версия не должна выглядеть недоделанной.
|
||||
|
||||
## Токены: `core/theme/app_theme.dart`
|
||||
|
||||
`AppTheme.light()` / `AppTheme.dark()` заменили инлайновые `ThemeData(...)` в `app.dart`.
|
||||
Обе строятся одной функцией `_build(Brightness)` — нет двух параллельных наборов чисел,
|
||||
которые могут разъехаться.
|
||||
`AppTheme.light()` / `AppTheme.dark()` строятся одной функцией `_build(Brightness)`.
|
||||
|
||||
- **Seed** — `Color(0xFF2A78D6)`, тот же синий, что `ChartColors.slot1Blue` (основной ряд
|
||||
графиков капитала/портфеля). До редизайна seed темы (`0xFF2E6F5E`, зелёно-бирюзовый) и
|
||||
цвет основной линии на графиках были разными брендовыми цветами одного приложения —
|
||||
теперь акцент интерфейса и «главный» цвет данных на графиках — один и тот же.
|
||||
- **Поверхности** — не переопределены вручную: `ColorScheme.fromSeed` в Material 3 уже
|
||||
тонирует нейтральную палитру оттенком seed, поэтому тёмный `surface` от синего seed
|
||||
получается тёмным сине-индиго сам по себе («не чистый чёрный», как в референсе), без
|
||||
захардкоженных цветов сверх seed. Слои `surfaceContainer(Low/…/High)` идут на карточки,
|
||||
панель навигации и фон страницы — так они остаются согласованно тонированными при смене
|
||||
темы.
|
||||
- **`cardTheme`** — скруглённые углы (16), `elevation: 0`, тонкая рамка `outlineVariant`
|
||||
вместо тени. Это глобальный токен: он каскадом меняет вид `Card` **везде**, где он уже
|
||||
используется (`SectionCard`, `StatTile`, `GoalCard`, карточки Здоровья, `_Card` в
|
||||
`holdings_tab.dart` и т.д.) без правки каждого экрана — что и даёт «зафиксировать токены
|
||||
один раз» смысл. Экраны, которые ещё не редизайнены явно, уже выглядят новее только за
|
||||
счёт этого.
|
||||
- **`navigationRailTheme` / `navigationBarTheme`** — акцентная подсветка активного пункта
|
||||
(`primary`/`primaryContainer`) для свёрнутого rail (600–1200) и нижнего бара (<600).
|
||||
- **Акцент** — `AppTheme.seed = 0xFF14AFFF` (тот же, что `ChartColors.slot1Blue`, главный ряд
|
||||
графиков). В светлой теме `primary` чуть темнее (`0xFF0A98E6`) ради контраста на белом.
|
||||
- **Тёмные поверхности** заданы явно поверх `ColorScheme.fromSeed`, потому что тонировка
|
||||
Material 3 даёт синий, а не графитово-фиолетовый:
|
||||
`surface` `#25252F` (фон страницы) → `surfaceContainerLowest` `#1F1F28` (верхняя панель) →
|
||||
`surfaceContainer` `#2F2F3C` (карточки) → `surfaceContainerHigh` `#3A3A49` (поля ввода,
|
||||
вторичные кнопки) → `surfaceContainerHighest` `#474757` (активная «таблетка» навигации).
|
||||
- **Плюс и минус** — `ChartColors.gain` (мятный `#3DDC97`) и `ChartColors.loss` (коралловый
|
||||
`#F26B6B`); `signColor` (`portfolio/labels.dart`) возвращает именно их. Категориальные цвета
|
||||
графиков (`slot1…slot5`) не менялись.
|
||||
- **`cardTheme`** — радиус 12, `elevation: 0`; в тёмной теме без рамки, в светлой с тонкой
|
||||
`outlineVariant`. Глобальный токен: меняет вид `Card` везде.
|
||||
- **`inputDecorationTheme`** — заливка `surfaceContainerHigh`, без обводки, радиус 8, акцентная
|
||||
рамка в фокусе. Действует на все формы и диалоги без правки экранов.
|
||||
- Кнопки (радиус 8), `tabBarTheme`, `dialogTheme`, `popupMenuTheme`, `chipTheme` — из тех же токенов.
|
||||
|
||||
Все цвета берутся из `ColorScheme` или уже существующего `ChartColors` — ни одного нового
|
||||
хардкод-цвета вне них.
|
||||
Все цвета берутся из `ColorScheme` или `ChartColors`; хардкод вне них — только градиент логотипа.
|
||||
|
||||
## Компоненты
|
||||
## Навигация: верхняя панель
|
||||
|
||||
- **`core/widgets/section_header.dart` → `SectionHeader`** — жирный заголовок + короткая
|
||||
акцентная полоска (28×3, `colorScheme.primary`) под левой частью текста. Замена голому
|
||||
`Text(title, style: titleMedium)` для смысловых блоков на странице (не для карточек со
|
||||
своим заголовком — там по-прежнему `SectionCard`/`_Card`).
|
||||
- **`core/widgets/tile_carousel.dart` → `TileCarousel`** — горизонтальная прокрутка вместо
|
||||
`Wrap` для рядов `StatTile`. На Обзоре заменила `Wrap` у капитала, месяца, портфеля и
|
||||
runway. Не требует общей высоты у детей (`IntrinsicHeight` внутри), поэтому подходит для
|
||||
разноразмерных карточек будущих экранов (счета, холдинги).
|
||||
- **`SectionCard`/`StatTile`** (`core/widgets/section_card.dart`) не переписаны — их новый
|
||||
вид целиком идёт из `cardTheme`. `home_page.dart` перестал дублировать их локальными
|
||||
`_ChartCard`/`_StatTile` и использует общие виджеты, как и остальные вкладки.
|
||||
`features/shell/top_nav.dart` (`TopNav`) показывается от 600 px и шире; уже 600 — нижний
|
||||
`NavigationBar` с «Ещё» (как раньше). Список направлений — `nav_destinations.dart`.
|
||||
|
||||
## `NavSidebar` (features/shell/nav_sidebar.dart)
|
||||
- **Слева** знак сервиса (`core/widgets/service_mark.dart`, тот же рисунок, что `web/favicon.svg`: диск с градиентом от акцента к фиолетовому и три растущих столбца; тап ведёт на Обзор) и четыре основные вкладки-«таблетки»: Обзор, Аналитика, Портфель, Счета
|
||||
(порядок как в Snowball). Экраны «Операции» (События, Импорт, Потоки, Категории, Операции,
|
||||
Правила) собраны в выпадающем меню «Операции ▾»; на нём же подписано имя открытого экрана.
|
||||
- **Справа** «Добавить» (событие, брокерский счёт, портфель, отчёт брокера), значок Здоровья со
|
||||
счётчиком замечаний (`dataQualityProvider`), Настройки, аватар с меню (email, «Выйти»).
|
||||
- До 900 px «Добавить» сжимается до иконки.
|
||||
- Контент центрируется и не шире 1440 px.
|
||||
|
||||
Показывается только на широком breakpoint (≥1200, `_wideBreakpoint` в `app_shell.dart`).
|
||||
На 600–1200 остаётся штатный `NavigationRail` в свёрнутом виде — в иконку-без-подписи всё
|
||||
равно не помещаются ни капс-заголовки групп, ни счётчики, ни профиль, так что городить туда
|
||||
кастомный виджет незачем; на <600 остаётся штатный `NavigationBar`. Список направлений
|
||||
вынесен в `features/shell/nav_destinations.dart` (`NavDestination`, `navDestinations`,
|
||||
`NavGroup`) — общий для `app_shell.dart` и `nav_sidebar.dart`, было приватным в одном файле.
|
||||
`NavSidebar` и `NavigationRail` удалены. Наблюдение за `dataQualityProvider` и `meProvider`
|
||||
остаётся в `TopNav`, значит эти запросы уходят при каждом заходе, а не только на трёх экранах;
|
||||
для одного пользователя это не проблема, но стоит знать при правке `core/cache/*`.
|
||||
|
||||
- **Группы** (`NavGroup`): «ОБЗОР» (Обзор/Счета/Портфель/Аналитика — те же четыре, что
|
||||
сейчас основные для нижнего бара), «ОПЕРАЦИИ» (События/Импорт/Потоки/Категории/
|
||||
Операции/Правила), «СИСТЕМА» (Здоровье/Настройки).
|
||||
- **Активный пункт** — левая полоска 3px `colorScheme.primary` + тонированный фон
|
||||
`primaryContainer` + акцентный цвет текста/иконки.
|
||||
- **Счётчик** — сейчас подключён только у «Здоровье», из `dataQualityProvider`
|
||||
(число строк). У «Цели» счётчика нет: это не отдельный пункт меню, а часть хаба
|
||||
«Аналитика» (`alsoMatches` в `NavDestination`) — добавлять его как отдельный пункт ради
|
||||
счётчика значило бы менять структуру навигации, что не входило в задачу «только
|
||||
визуальный слой».
|
||||
- **Профиль внизу** — аватар с инициалом + email из `meProvider`, «имя» — локальная часть
|
||||
email до `@` (в бэкенде нет отдельного отображаемого имени, только email/id).
|
||||
## Экраны
|
||||
|
||||
**Осознанный побочный эффект**: `NavSidebar` вызывает `ref.watch` на `dataQualityProvider`
|
||||
и `meProvider`, которые раньше запрашивались только при открытии Обзора/Здоровья/Настроек.
|
||||
Поскольку `AppShell` смонтирован всё время сессии, эти два запроса теперь улетают на API
|
||||
при каждом заходе в приложение, а не только на этих трёх экранах. `meProvider` не
|
||||
`autoDispose` и уже держится живым где-то ещё — здесь она просто донат к уже открытому
|
||||
запросу. `dataQualityProvider` — `autoDispose`; watch из шелла держит его живым, пока
|
||||
приложение открыто. Для одиночного пользователя на своём VPS это не проблема
|
||||
производительности, но это решение, а не то, о чём просили буквально — стоит знать при
|
||||
следующей правке `core/cache/*`.
|
||||
- **Обзор** — сверху сетка карточек «Портфели»: по одной на `all`, портфель и счёт
|
||||
(`home/scope_cards.dart`). В карточке: стоимость, прибыль (₽ и %), за день, доходность
|
||||
(XIRR с начала), пассивный доход (% и ₽ в год). Один запрос `GET /analytics/overview`;
|
||||
тап открывает Портфель в этом скоупе. Ниже прежние блоки (капитал, месяц, запас хода, графики).
|
||||
- **Аналитика** — общая полоса вкладок под верхней панелью (`shell/analytics_tabs.dart`):
|
||||
Общее, Дивиденды, Ребалансировка, Цели, Налоги, Портфели. Каждая вкладка — свой маршрут
|
||||
(`/analytics`, `/income`, …), остаются ссылаемыми. «Общее» (`AnalyticsHubPage`) — четыре
|
||||
плитки (Стоимость, Прибыль, Доходность, Пассивный доход; `portfolio/overview_tiles.dart`),
|
||||
график стоимости, доходность, бенчмарки. На телефоне вместо полосы остаётся список разделов.
|
||||
- **Портфель** — таблица активов (`portfolio/holdings_table.dart`) с вкладками-пресетами колонок
|
||||
(Мои активы, Общее, Дивиденды, Прибыль, Облигации), поиском и двухстрочными ячейками
|
||||
(сумма и, ниже, цена за штуку или доля). Колонки растягиваются на карточку, при нехватке места
|
||||
таблица прокручивается вбок. Неизвестная величина — «—», а не «0 ₽»; нулевые дивиденды тоже «—».
|
||||
`HoldingsTab` принимает `HoldingsSections` (`all`, `table`, `overview`), чтобы Портфель и
|
||||
«Аналитика → Общее» брали одни и те же провайдеры и не могли разойтись в числах.
|
||||
- **Счета** — один компактный список (`accounts/accounts_page.dart`): поиск, чипы статуса с числами (Все / Активные / Отключённые / Архивные) и типа (ликвидные, сбережения, инвестиции, долги). В строке — источник (ZenMoney, T-Invest, отчёты), тип-«таблетка» с меню, баланс или стоимость, переключатели «В капитал» и «Активен». Отключённые и архивные приглушены и идут в конце. Чекбоксы включают панель массовых действий (отключить, включить, сменить тип, учёт в капитале) — по одному запросу на счёт и один пересчёт метрик на всю пачку (`accounts/actions.dart`). До 880 px строка двухстрочная.
|
||||
- **Настройки** — карточка с вкладками сверху (Аккаунт, Отображение, Сервер) и меню разделов
|
||||
слева; до 720 px меню превращается в ряд чипов.
|
||||
|
||||
## Что не тронуто в этой фазе
|
||||
## Что не менялось
|
||||
|
||||
- `health_page.dart`, `holdings_tab.dart`, `goal_card.dart` — контент не менялся.
|
||||
`SectionHeader` для «Требует внимания»/списка запусков на Здоровье и разбор карточек
|
||||
холдингов/целей под бейдж+прогресс-полоску (как обложки Grimmory) — следующий шаг, тем же
|
||||
способом: один экран, проверка, потом дальше.
|
||||
- Маршруты и провайдеры не менялись, кроме двух новых точек `ref.watch` в `NavSidebar`
|
||||
(см. выше).
|
||||
Экраны Счета, События, Импорт, Потоки, Категории, Операции, Правила, Здоровье, Цели, Налоги,
|
||||
Ребалансировка, карточка инструмента сохранили раскладку и подхватили токены (карточки, поля,
|
||||
кнопки, вкладки). Переделывать их по образцу Snowball можно тем же способом: экран за экраном.
|
||||
|
||||
## Иконки активов
|
||||
|
||||
`AssetIcon` (`core/widgets/asset_icon.dart`) — круг с логотипом эмитента; стоит в колонке «Актив» таблицы
|
||||
и в шапке карточки инструмента. Логотип берётся из брендовых данных T-Invest: синк раскладывает
|
||||
`brand.logo_name` и `brand.logo_base_color` по `instrument.logo_name` / `logo_color` (сначала по
|
||||
`tinvest_uid`, потом по ISIN — так подхватываются и бумаги из отчётов Сбера и ВТБ; заполненное не
|
||||
перезаписывается). API отдаёт готовый `logo_url` (`branding.logo_url`, публичный CDN
|
||||
`invest-brands.cdn-tinkoff.ru`, 160 px, без ключа, с `access-control-allow-origin: *`) и `logo_color`.
|
||||
Нет логотипа, он ещё грузится или не загрузился — круг цвета бренда (нейтральный, если цвета нет)
|
||||
с иконкой класса актива (`assetClassIcon`), так что подписи по типу остаются запасным вариантом.
|
||||
|
||||
## Подсказки к терминам
|
||||
|
||||
Рядом с терминами и аббревиатурами (XIRR, TWR, НКД, ЛДВ, «Запас хода», «Пассивный доход» и т. д.)
|
||||
стоит приглушённый значок «?»: на вебе и десктопе подсказка появляется по наведению, на телефоне
|
||||
по тапу (`core/widgets/help_tip.dart`, `HelpTip`).
|
||||
|
||||
- **`TermLabel(text)`** — подпись, которая сама ищет объяснение в словаре
|
||||
(`core/glossary.dart`, `glossaryHint`) по тексту подписи (без учёта регистра и «ё»). Нет записи —
|
||||
обычный `Text`, поэтому его безопасно ставить под любую подпись. Параметр `hint` перекрывает
|
||||
словарь там, где одно слово значит разное (например, «Прибыль» в налогах).
|
||||
- `StatTile` и `SectionCard` уже рисуют подписи через `TermLabel`, поэтому Обзор, доходы, цели и
|
||||
налоги получили подсказки без правки экранов; заголовки таблиц (Портфель, Налоги), карточки на
|
||||
Обзоре, плитки Аналитики, детали события и карточка инструмента — тоже.
|
||||
- **Новый термин** — добавить строку в `_entries` словаря; определения сверяйте с кодом аналитики
|
||||
(`analytics/*.py`), а не с общими представлениями: например, запас хода считается от «обычного»
|
||||
расхода за три последних полных месяца и не включает инвестиции.
|
||||
|
||||
Reference in New Issue
Block a user