Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
182 lines
6.8 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|