feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события

Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
Dmitry
2026-09-19 22:14:21 +03:00
parent a559d6de3e
commit 62d36aa3e8
73 changed files with 6406 additions and 1192 deletions
+53 -10
View File
@@ -1,15 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
/// Аналитика: a hub for the phase-4 screens.
import '../portfolio/holdings_tab.dart';
import '../portfolio/overview_tiles.dart';
import '../portfolio/scope_selector.dart';
/// Аналитика → Общее. On a wide surface it is Snowball's overview — the four headline figures
/// for the chosen scope, then the value chart, returns and benchmark comparison; the other
/// analytics screens are one tab away (`AnalyticsTabs`). On a phone there is no tab strip, so
/// the same entry is a list of the sections instead.
///
/// Why a hub instead of four more destinations: the shell already carried eleven, which on
/// a phone leaves ~36 px per label in the bottom bar. Доходы, Ребалансировка, Цели and
/// Налоги are all "what do I do with the portfolio next" questions, so they live behind one
/// entry that highlights for all four (see `alsoMatches` in `app_shell.dart`), while each
/// keeps its own top-level route from the contract (`/income`, `/rebalance`, `/goals`,
/// `/tax`) and is deep-linkable.
class AnalyticsHubPage extends StatelessWidget {
/// The sections keep their own top-level routes from the contract (`/income`, `/rebalance`,
/// `/goals`, `/tax`) and are deep-linkable; `alsoMatches` in `nav_destinations.dart` keeps
/// Аналитика highlighted inside any of them.
class AnalyticsHubPage extends ConsumerWidget {
const AnalyticsHubPage({super.key});
static const _entries = [
@@ -23,7 +28,8 @@ class AnalyticsHubPage extends StatelessWidget {
path: '/rebalance',
icon: Icons.balance,
title: 'Ребалансировка',
subtitle: 'Целевые веса портфеля и рекомендации, что докупить или продать',
subtitle:
'Целевые веса портфеля и рекомендации, что докупить или продать',
),
(
path: '/goals',
@@ -39,6 +45,43 @@ class AnalyticsHubPage extends StatelessWidget {
),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
if (MediaQuery.sizeOf(context).width >= 600) return const _Overview();
return const _SectionList();
}
}
class _Overview extends StatelessWidget {
const _Overview();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
const Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Align(
alignment: Alignment.centerRight,
child: ScopeSelector(),
),
),
const Expanded(
child: HoldingsTab(
sections: HoldingsSections.overview,
header: OverviewTiles(),
),
),
],
),
);
}
}
class _SectionList extends StatelessWidget {
const _SectionList();
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -46,7 +89,7 @@ class AnalyticsHubPage extends StatelessWidget {
body: ListView(
padding: const EdgeInsets.all(16),
children: [
for (final e in _entries)
for (final e in AnalyticsHubPage._entries)
Card(
child: ListTile(
leading: Icon(e.icon),