diff --git a/docs/ai/README.md b/docs/ai/README.md index b08cd67..6c1c0f5 100644 --- a/docs/ai/README.md +++ b/docs/ai/README.md @@ -37,3 +37,5 @@ worker (APScheduler, advisory locks) ▼ - [links.md](links.md) — внешние API и референсы. - [offline-cache.md](offline-cache.md) — офлайн-кэш Flutter-клиента: контракт `Cached`, `CacheInterceptor`, баннер «данные на …» (фаза 5). +- [design-system.md](design-system.md) — визуальный язык Flutter-клиента: токены темы, + `SectionHeader`/`TileCarousel`, группированный `NavSidebar` (обкатано на Обзоре). diff --git a/docs/ai/design-system.md b/docs/ai/design-system.md new file mode 100644 index 0000000..232c8a7 --- /dev/null +++ b/docs/ai/design-system.md @@ -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` + (см. выше).