feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// An asset's picture: the issuer's logo from the broker, and while it loads or when the
|
||||
/// broker has none, a circle in the brand colour with the icon of the asset class.
|
||||
///
|
||||
/// The logo URL and colour come from the API as they are; a failed load (offline, a logo
|
||||
/// the CDN has since dropped) quietly falls back rather than showing a broken image.
|
||||
class AssetIcon extends StatelessWidget {
|
||||
const AssetIcon({
|
||||
super.key,
|
||||
required this.assetClass,
|
||||
this.logoUrl,
|
||||
this.logoColor,
|
||||
this.size = 32,
|
||||
});
|
||||
|
||||
final String? assetClass;
|
||||
final String? logoUrl;
|
||||
final String? logoColor;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final url = logoUrl;
|
||||
final fallback = _Fallback(
|
||||
assetClass: assetClass,
|
||||
logoColor: logoColor,
|
||||
size: size,
|
||||
);
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: ClipOval(
|
||||
child: url == null
|
||||
? fallback
|
||||
: Image.network(
|
||||
url,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.medium,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => fallback,
|
||||
loadingBuilder: (_, child, progress) =>
|
||||
progress == null ? child : fallback,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Fallback extends StatelessWidget {
|
||||
const _Fallback({
|
||||
required this.assetClass,
|
||||
required this.logoColor,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final String? assetClass;
|
||||
final String? logoColor;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final brand = parseBrandColor(logoColor);
|
||||
final background = brand ?? scheme.surfaceContainerHigh;
|
||||
final foreground = brand == null
|
||||
? scheme.onSurfaceVariant
|
||||
: (ThemeData.estimateBrightnessForColor(brand) == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black87);
|
||||
return ColoredBox(
|
||||
color: background,
|
||||
child: Icon(
|
||||
assetClassIcon(assetClass),
|
||||
size: size * 0.55,
|
||||
color: foreground,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `#21A038` (or `#AA21A038`) as a colour; null for anything else, so a malformed value
|
||||
/// from the broker degrades to the neutral fallback instead of throwing.
|
||||
Color? parseBrandColor(String? hex) {
|
||||
if (hex == null) return null;
|
||||
final digits = hex.startsWith('#') ? hex.substring(1) : hex;
|
||||
if (digits.length != 6 && digits.length != 8) return null;
|
||||
final value = int.tryParse(digits, radix: 16);
|
||||
if (value == null) return null;
|
||||
return Color(digits.length == 6 ? 0xFF000000 | value : value);
|
||||
}
|
||||
|
||||
IconData assetClassIcon(String? assetClass) => switch (assetClass) {
|
||||
'share' => Icons.trending_up,
|
||||
'bond' => Icons.receipt_long_outlined,
|
||||
'etf' || 'fund' => Icons.pie_chart_outline,
|
||||
'currency' => Icons.currency_exchange,
|
||||
'index' => Icons.show_chart,
|
||||
'deposit' => Icons.savings_outlined,
|
||||
'real_estate' => Icons.home_outlined,
|
||||
'crypto' => Icons.currency_bitcoin,
|
||||
_ => Icons.circle_outlined,
|
||||
};
|
||||
Reference in New Issue
Block a user