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

182 lines
6.8 KiB
Dart

import 'package:flutter/material.dart';
/// 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 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(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 = _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,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: scheme.surface,
appBarTheme: AppBarTheme(
backgroundColor: scheme.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
),
cardTheme: CardThemeData(
elevation: 0,
color: scheme.surfaceContainer,
surfaceTintColor: Colors.transparent,
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
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,
),
unselectedLabelTextStyle: TextStyle(color: scheme.onSurfaceVariant),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: scheme.surfaceContainerLowest,
indicatorColor: scheme.primaryContainer,
elevation: 0,
iconTheme: WidgetStateProperty.resolveWith(
(states) => IconThemeData(
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,
),
),
),
);
}
}