docs: зафиксировать визуальный язык клиента в design-system.md

Описывает токены темы, SectionHeader/TileCarousel, NavSidebar и
осознанные побочные эффекты (dataQualityProvider/meProvider watch из
шелла), а также что ещё не редизайнено — health_page.dart и карточки
holdings_tab.dart/goal_card.dart.
This commit is contained in:
Dmitry
2026-09-19 15:32:00 +03:00
parent e4a64cd1df
commit 989a780f9f
2 changed files with 106 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
# Визуальный язык (фаза 6, начата)
Редизайн Flutter-клиента в духе Grimmory (self-hosted книжная библиотека): тёмный сине-
индиго фон вместо нейтрального серого, плоские карточки с рамкой вместо тени, группированная
боковая навигация с капс-заголовками и левой акцентной полоской у активного пункта. Обкатано
целиком на одном экране — Обзор (`features/home/home_page.dart`) — и на навигационном шелле
(он общий для всех экранов, поэтому не в счёт «раскатки»), прежде чем идти дальше, тем же
способом, что офлайн-кэш (`offline-cache.md`).
## Решение: одна тема на обе яркости, не «тёмная как есть — светлая как получится»
Тёмная и светлая тема получают один и тот же язык (форма карточек, акцент, поведение
навигации), различаются только тона поверхностей. Причины:
- Приложение живёт на вебе, Linux, Windows и Android одновременно — пользователь может
оказаться в любой теме по системной настройке, и она уже переключается в
`settings_page.dart`. Бесплатно смотрящаяся только тёмная версия читалась бы как
недоделанная в светлой.
- Семантика цвета (`signColor` — прибыль/убыток, `ChartColors` — ряды графиков) уже общая
для обеих тем; заводить для тёмной темы отдельный «фирменный» акцент, а для светлой
оставлять дефолтный Material-синий было бы второй, несогласованной системой токенов.
## Токены: `core/theme/app_theme.dart`
`AppTheme.light()` / `AppTheme.dark()` заменили инлайновые `ThemeData(...)` в `app.dart`.
Обе строятся одной функцией `_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).
Все цвета берутся из `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` и использует общие виджеты, как и остальные вкладки.
## `NavSidebar` (features/shell/nav_sidebar.dart)
Показывается только на широком 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`, было приватным в одном файле.
- **Группы** (`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/*`.
## Что не тронуто в этой фазе
- `health_page.dart`, `holdings_tab.dart`, `goal_card.dart` — контент не менялся.
`SectionHeader` для «Требует внимания»/списка запусков на Здоровье и разбор карточек
холдингов/целей под бейдж+прогресс-полоску (как обложки Grimmory) — следующий шаг, тем же
способом: один экран, проверка, потом дальше.
- Маршруты и провайдеры не менялись, кроме двух новых точек `ref.watch` в `NavSidebar`
(см. выше).