Files
fin-tracker/docs/ai/design-system.md
T
Dmitry 989a780f9f docs: зафиксировать визуальный язык клиента в design-system.md
Описывает токены темы, SectionHeader/TileCarousel, NavSidebar и
осознанные побочные эффекты (dataQualityProvider/meProvider watch из
шелла), а также что ещё не редизайнено — health_page.dart и карточки
holdings_tab.dart/goal_card.dart.
2026-09-19 15:32:00 +03:00

10 KiB

Визуальный язык (фаза 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) — нет двух параллельных наборов чисел, которые могут разъехаться.

  • SeedColor(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.dartSectionHeader — жирный заголовок + короткая акцентная полоска (28×3, colorScheme.primary) под левой частью текста. Замена голому Text(title, style: titleMedium) для смысловых блоков на странице (не для карточек со своим заголовком — там по-прежнему SectionCard/_Card).
  • core/widgets/tile_carousel.dartTileCarousel — горизонтальная прокрутка вместо 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 и уже держится живым где-то ещё — здесь она просто донат к уже открытому запросу. dataQualityProviderautoDispose; watch из шелла держит его живым, пока приложение открыто. Для одиночного пользователя на своём VPS это не проблема производительности, но это решение, а не то, о чём просили буквально — стоит знать при следующей правке core/cache/*.

Что не тронуто в этой фазе

  • health_page.dart, holdings_tab.dart, goal_card.dart — контент не менялся. SectionHeader для «Требует внимания»/списка запусков на Здоровье и разбор карточек холдингов/целей под бейдж+прогресс-полоску (как обложки Grimmory) — следующий шаг, тем же способом: один экран, проверка, потом дальше.
  • Маршруты и провайдеры не менялись, кроме двух новых точек ref.watch в NavSidebar (см. выше).