Files
Dmitry 62d36aa3e8 feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
2026-09-19 22:14:21 +03:00

142 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'analytics_tabs.dart';
import 'nav_destinations.dart';
import 'top_nav.dart';
/// Below this width the shell falls back to a bottom [NavigationBar]; from here up it is the
/// [TopNav] bar.
const _narrowBreakpoint = 600.0;
/// Adaptive navigation shell around the current route: a bottom [NavigationBar] on narrow
/// surfaces and the [TopNav] bar on wider ones, with the Аналитика tab strip under it while
/// one of the analytics screens is open.
///
/// The top bar shows every destination (the ledger ones behind «Операции»). The bottom bar
/// shows the four primary ones plus «Ещё», which opens the rest in a sheet: twelve
/// destinations in a phone-width bar would leave about 30 px per label, which is not
/// navigation but decoration.
class AppShell extends StatelessWidget {
const AppShell({required this.location, required this.child, super.key});
final String location;
final Widget child;
int get _selectedIndex {
// longest prefix wins, so /portfolio/instrument/311 keeps Портфель selected
var best = -1;
var bestLength = -1;
for (var i = 0; i < navDestinations.length; i++) {
final length = navDestinations[i].matchLength(location);
if (length > bestLength) {
best = i;
bestLength = length;
}
}
return best == -1 || bestLength < 0 ? 0 : best;
}
void _onSelect(BuildContext context, int index) {
if (index != _selectedIndex) context.go(navDestinations[index].path);
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
if (width < _narrowBreakpoint) {
final primary = navDestinations.where((d) => d.primary).toList();
final selected = navDestinations[_selectedIndex];
final primaryIndex = primary.indexOf(selected);
return Scaffold(
body: SafeArea(child: child),
bottomNavigationBar: NavigationBar(
// nothing primary matches ⇒ we are inside a screen that lives behind «Ещё»,
// and «Ещё» is what should look active
selectedIndex: primaryIndex >= 0 ? primaryIndex : primary.length,
onDestinationSelected: (i) {
if (i >= primary.length) {
_showMore(context, selected);
} else {
final target = navDestinations.indexOf(primary[i]);
_onSelect(context, target);
}
},
destinations: [
for (final d in primary)
NavigationDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: d.label,
),
NavigationDestination(
icon: const Icon(Icons.more_horiz),
selectedIcon: const Icon(Icons.more_horiz),
label: primaryIndex >= 0 ? 'Ещё' : selected.label,
),
],
),
);
}
return Scaffold(
body: Column(
children: [
TopNav(
selectedIndex: _selectedIndex,
onSelect: (i) => _onSelect(context, i),
),
Expanded(
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kContentMaxWidth),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (AnalyticsTabs.contains(location))
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: AnalyticsTabs(location: location),
),
Expanded(child: child),
],
),
),
),
),
],
),
);
}
/// The non-primary destinations, as a sheet. The currently open one is ticked, so «Ещё»
/// still answers "where am I".
void _showMore(BuildContext context, NavDestination current) {
showModalBottomSheet<void>(
context: context,
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: ListView(
shrinkWrap: true,
children: [
for (final d in navDestinations.where((d) => !d.primary))
ListTile(
leading: Icon(d == current ? d.selectedIcon : d.icon),
title: Text(d.label),
selected: d == current,
trailing: d == current ? const Icon(Icons.check) : null,
onTap: () {
Navigator.of(sheetContext).pop();
if (d != current) context.go(d.path);
},
),
],
),
),
);
}
}