feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
@@ -1,25 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Design tokens for fin-tracker's visual language (`docs/ai/design-system.md`): dark
|
||||
/// surfaces tinted blue-indigo rather than neutral grey, flat bordered cards instead of
|
||||
/// Material's default elevation shadow, and one accent seed shared with
|
||||
/// `ChartColors.slot1Blue` so UI chrome and the primary data series read as the same color.
|
||||
/// Design tokens for fin-tracker's visual language (`docs/ai/design-system.md`): graphite-violet
|
||||
/// surfaces, one bright sky-blue accent, flat rounded cards with no outline, filled inputs and
|
||||
/// pill-shaped navigation — the look of Snowball.
|
||||
///
|
||||
/// Both brightnesses share this seed and this shape language — only the surface tones
|
||||
/// Both brightnesses share this accent and this shape language — only the surface tones
|
||||
/// differ — so the dark theme is not a special case bolted onto a default light Material
|
||||
/// theme, and switching `themeModeProvider` never changes which widgets exist.
|
||||
class AppTheme {
|
||||
const AppTheme._();
|
||||
|
||||
/// The accent: buttons, active tabs, links and the primary chart series.
|
||||
/// Matches `ChartColors.slot1Blue`.
|
||||
static const seed = Color(0xFF2A78D6);
|
||||
static const seed = Color(0xFF14AFFF);
|
||||
|
||||
static ThemeData light() => _build(Brightness.light);
|
||||
static ThemeData dark() => _build(Brightness.dark);
|
||||
|
||||
static ColorScheme _scheme(Brightness brightness) {
|
||||
final base = ColorScheme.fromSeed(seedColor: seed, brightness: brightness);
|
||||
if (brightness == Brightness.light) {
|
||||
return base.copyWith(
|
||||
primary: const Color(0xFF0A98E6),
|
||||
onPrimary: Colors.white,
|
||||
surface: const Color(0xFFF2F2F7),
|
||||
surfaceContainerLowest: Colors.white,
|
||||
surfaceContainerLow: const Color(0xFFF7F7FB),
|
||||
surfaceContainer: Colors.white,
|
||||
surfaceContainerHigh: const Color(0xFFE8E8F0),
|
||||
surfaceContainerHighest: const Color(0xFFDCDCE6),
|
||||
outlineVariant: const Color(0xFFD8D8E2),
|
||||
);
|
||||
}
|
||||
return base.copyWith(
|
||||
primary: seed,
|
||||
onPrimary: const Color(0xFF04283B),
|
||||
primaryContainer: const Color(0xFF0E4B6E),
|
||||
onPrimaryContainer: const Color(0xFFD3F0FF),
|
||||
surface: const Color(0xFF25252F),
|
||||
surfaceContainerLowest: const Color(0xFF1F1F28),
|
||||
surfaceContainerLow: const Color(0xFF2A2A35),
|
||||
surfaceContainer: const Color(0xFF2F2F3C),
|
||||
surfaceContainerHigh: const Color(0xFF3A3A49),
|
||||
surfaceContainerHighest: const Color(0xFF474757),
|
||||
onSurface: const Color(0xFFEDEDF3),
|
||||
onSurfaceVariant: const Color(0xFFB4B4C3),
|
||||
outline: const Color(0xFF74748A),
|
||||
outlineVariant: const Color(0xFF3F3F4F),
|
||||
error: const Color(0xFFF26B6B),
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData _build(Brightness brightness) {
|
||||
final scheme = ColorScheme.fromSeed(seedColor: seed, brightness: brightness);
|
||||
final outline = scheme.outlineVariant.withValues(alpha: brightness == Brightness.dark ? 0.4 : 0.7);
|
||||
final scheme = _scheme(brightness);
|
||||
final dark = brightness == Brightness.dark;
|
||||
final outline = scheme.outlineVariant;
|
||||
final radius = BorderRadius.circular(12);
|
||||
final fieldRadius = BorderRadius.circular(8);
|
||||
|
||||
OutlineInputBorder field([Color? color, double width = 1]) =>
|
||||
OutlineInputBorder(
|
||||
borderRadius: fieldRadius,
|
||||
borderSide: color == null
|
||||
? BorderSide.none
|
||||
: BorderSide(color: color, width: width),
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
@@ -39,34 +84,95 @@ class AppTheme {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: outline),
|
||||
borderRadius: radius,
|
||||
side: dark ? BorderSide.none : BorderSide(color: outline),
|
||||
),
|
||||
),
|
||||
dividerTheme: DividerThemeData(color: outline, space: 1, thickness: 1),
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: scheme.surfaceContainer,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: scheme.surfaceContainer,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: scheme.surfaceContainerHigh,
|
||||
border: field(),
|
||||
enabledBorder: field(),
|
||||
focusedBorder: field(scheme.primary, 1.5),
|
||||
errorBorder: field(scheme.error),
|
||||
focusedErrorBorder: field(scheme.error, 1.5),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: fieldRadius),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: fieldRadius),
|
||||
side: BorderSide(color: outline),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: fieldRadius),
|
||||
),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: scheme.surfaceContainerHigh,
|
||||
side: BorderSide.none,
|
||||
shape: RoundedRectangleBorder(borderRadius: fieldRadius),
|
||||
),
|
||||
tabBarTheme: TabBarThemeData(
|
||||
indicatorColor: scheme.primary,
|
||||
labelColor: scheme.onSurface,
|
||||
unselectedLabelColor: scheme.onSurfaceVariant,
|
||||
dividerColor: outline,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
color: scheme.surfaceContainerHigh,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(borderRadius: radius),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
indicatorColor: scheme.primaryContainer,
|
||||
useIndicator: true,
|
||||
selectedIconTheme: IconThemeData(color: scheme.primary),
|
||||
unselectedIconTheme: IconThemeData(color: scheme.onSurfaceVariant),
|
||||
selectedLabelTextStyle: TextStyle(color: scheme.primary, fontWeight: FontWeight.w600),
|
||||
selectedLabelTextStyle: TextStyle(
|
||||
color: scheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelTextStyle: TextStyle(color: scheme.onSurfaceVariant),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
backgroundColor: scheme.surfaceContainerLowest,
|
||||
indicatorColor: scheme.primaryContainer,
|
||||
elevation: 0,
|
||||
iconTheme: WidgetStateProperty.resolveWith(
|
||||
(states) => IconThemeData(
|
||||
color: states.contains(WidgetState.selected) ? scheme.primary : scheme.onSurfaceVariant,
|
||||
color: states.contains(WidgetState.selected)
|
||||
? scheme.primary
|
||||
: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith(
|
||||
(states) => TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: states.contains(WidgetState.selected) ? FontWeight.w600 : FontWeight.w400,
|
||||
color: states.contains(WidgetState.selected) ? scheme.primary : scheme.onSurfaceVariant,
|
||||
fontWeight: states.contains(WidgetState.selected)
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
color: states.contains(WidgetState.selected)
|
||||
? scheme.primary
|
||||
: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,12 +6,17 @@ import 'package:flutter/material.dart';
|
||||
class ChartColors {
|
||||
const ChartColors._();
|
||||
|
||||
static const slot1Blue = Color(0xFF2A78D6);
|
||||
static const slot1Blue = Color(0xFF14AFFF);
|
||||
static const slot2Orange = Color(0xFFEB6834);
|
||||
static const slot3Aqua = Color(0xFF1BAF7A);
|
||||
static const slot4Yellow = Color(0xFFEDA100);
|
||||
static const slot5Magenta = Color(0xFFE87BA4);
|
||||
|
||||
/// Gain and loss, everywhere a number is signed (`signColor`): mint and coral, readable on
|
||||
/// both the graphite and the light surfaces.
|
||||
static const gain = Color(0xFF3DDC97);
|
||||
static const loss = Color(0xFFF26B6B);
|
||||
|
||||
/// This app's fixed assignment for cashflow charts.
|
||||
static const income = slot1Blue;
|
||||
static const expense = slot2Orange;
|
||||
|
||||
@@ -4,7 +4,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const _prefsKey = 'theme_mode';
|
||||
|
||||
final themeModeProvider = NotifierProvider<ThemeModeController, ThemeMode>(ThemeModeController.new);
|
||||
final themeModeProvider = NotifierProvider<ThemeModeController, ThemeMode>(
|
||||
ThemeModeController.new,
|
||||
);
|
||||
|
||||
/// Persists the chosen [ThemeMode] to this device via `shared_preferences`.
|
||||
class ThemeModeController extends Notifier<ThemeMode> {
|
||||
|
||||
Reference in New Issue
Block a user