Move map visuals into theme tokens
Build and deploy Flutter Web / build (push) Successful in 2m15s

This commit is contained in:
Ruslan Bakiev
2026-06-12 14:41:11 +07:00
parent 2853cc2581
commit 479dc25847
4 changed files with 134 additions and 48 deletions
+60
View File
@@ -9,6 +9,16 @@ class MapflowThemeTokens extends ThemeExtension<MapflowThemeTokens> {
required this.mapShadow, required this.mapShadow,
required this.danger, required this.danger,
required this.onDark, required this.onDark,
required this.darkSurface,
required this.darkSurfaceAlt,
required this.voiceAccent,
required this.voiceAccentSoft,
required this.onVoiceControl,
required this.snowflakeLine,
required this.snowflakeNode,
required this.snowflakeCenter,
required this.snowflakeLabel,
required this.imageScrim,
required this.panelRadius, required this.panelRadius,
}); });
@@ -17,6 +27,16 @@ class MapflowThemeTokens extends ThemeExtension<MapflowThemeTokens> {
final Color mapShadow; final Color mapShadow;
final Color danger; final Color danger;
final Color onDark; final Color onDark;
final Color darkSurface;
final Color darkSurfaceAlt;
final Color voiceAccent;
final Color voiceAccentSoft;
final Color onVoiceControl;
final Color snowflakeLine;
final Color snowflakeNode;
final Color snowflakeCenter;
final Color snowflakeLabel;
final Color imageScrim;
final double panelRadius; final double panelRadius;
@override @override
@@ -26,6 +46,16 @@ class MapflowThemeTokens extends ThemeExtension<MapflowThemeTokens> {
Color? mapShadow, Color? mapShadow,
Color? danger, Color? danger,
Color? onDark, Color? onDark,
Color? darkSurface,
Color? darkSurfaceAlt,
Color? voiceAccent,
Color? voiceAccentSoft,
Color? onVoiceControl,
Color? snowflakeLine,
Color? snowflakeNode,
Color? snowflakeCenter,
Color? snowflakeLabel,
Color? imageScrim,
double? panelRadius, double? panelRadius,
}) { }) {
return MapflowThemeTokens( return MapflowThemeTokens(
@@ -34,6 +64,16 @@ class MapflowThemeTokens extends ThemeExtension<MapflowThemeTokens> {
mapShadow: mapShadow ?? this.mapShadow, mapShadow: mapShadow ?? this.mapShadow,
danger: danger ?? this.danger, danger: danger ?? this.danger,
onDark: onDark ?? this.onDark, onDark: onDark ?? this.onDark,
darkSurface: darkSurface ?? this.darkSurface,
darkSurfaceAlt: darkSurfaceAlt ?? this.darkSurfaceAlt,
voiceAccent: voiceAccent ?? this.voiceAccent,
voiceAccentSoft: voiceAccentSoft ?? this.voiceAccentSoft,
onVoiceControl: onVoiceControl ?? this.onVoiceControl,
snowflakeLine: snowflakeLine ?? this.snowflakeLine,
snowflakeNode: snowflakeNode ?? this.snowflakeNode,
snowflakeCenter: snowflakeCenter ?? this.snowflakeCenter,
snowflakeLabel: snowflakeLabel ?? this.snowflakeLabel,
imageScrim: imageScrim ?? this.imageScrim,
panelRadius: panelRadius ?? this.panelRadius, panelRadius: panelRadius ?? this.panelRadius,
); );
} }
@@ -52,6 +92,16 @@ class MapflowThemeTokens extends ThemeExtension<MapflowThemeTokens> {
mapShadow: Color.lerp(mapShadow, other.mapShadow, t)!, mapShadow: Color.lerp(mapShadow, other.mapShadow, t)!,
danger: Color.lerp(danger, other.danger, t)!, danger: Color.lerp(danger, other.danger, t)!,
onDark: Color.lerp(onDark, other.onDark, t)!, onDark: Color.lerp(onDark, other.onDark, t)!,
darkSurface: Color.lerp(darkSurface, other.darkSurface, t)!,
darkSurfaceAlt: Color.lerp(darkSurfaceAlt, other.darkSurfaceAlt, t)!,
voiceAccent: Color.lerp(voiceAccent, other.voiceAccent, t)!,
voiceAccentSoft: Color.lerp(voiceAccentSoft, other.voiceAccentSoft, t)!,
onVoiceControl: Color.lerp(onVoiceControl, other.onVoiceControl, t)!,
snowflakeLine: Color.lerp(snowflakeLine, other.snowflakeLine, t)!,
snowflakeNode: Color.lerp(snowflakeNode, other.snowflakeNode, t)!,
snowflakeCenter: Color.lerp(snowflakeCenter, other.snowflakeCenter, t)!,
snowflakeLabel: Color.lerp(snowflakeLabel, other.snowflakeLabel, t)!,
imageScrim: Color.lerp(imageScrim, other.imageScrim, t)!,
panelRadius: lerpDouble(panelRadius, other.panelRadius, t), panelRadius: lerpDouble(panelRadius, other.panelRadius, t),
); );
} }
@@ -66,6 +116,16 @@ class MapflowTheme {
mapShadow: Color(0x33000000), mapShadow: Color(0x33000000),
danger: Color(0xFFE11D48), danger: Color(0xFFE11D48),
onDark: Colors.white, onDark: Colors.white,
darkSurface: Color(0xFF05030B),
darkSurfaceAlt: Color(0xFF15111D),
voiceAccent: Color(0xFFFF2D75),
voiceAccentSoft: Color(0xFFFF7A90),
onVoiceControl: Color(0xFF090613),
snowflakeLine: Color(0xFFE6DDD2),
snowflakeNode: Color(0xFFDED3C7),
snowflakeCenter: Color(0xFF241B18),
snowflakeLabel: Color(0xFF746A60),
imageScrim: Color(0xCC000000),
panelRadius: 8, panelRadius: 8,
); );
@@ -165,6 +165,8 @@ class _AdminOntologySnowflake extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
return SizedBox( return SizedBox(
height: 300, height: 300,
width: double.infinity, width: double.infinity,
@@ -172,6 +174,7 @@ class _AdminOntologySnowflake extends StatelessWidget {
painter: _AdminOntologySnowflakePainter( painter: _AdminOntologySnowflakePainter(
axes: _axes, axes: _axes,
selectedTags: selectedTags, selectedTags: selectedTags,
tokens: tokens,
), ),
), ),
); );
@@ -182,10 +185,12 @@ class _AdminOntologySnowflakePainter extends CustomPainter {
const _AdminOntologySnowflakePainter({ const _AdminOntologySnowflakePainter({
required this.axes, required this.axes,
required this.selectedTags, required this.selectedTags,
required this.tokens,
}); });
final List<_AdminOntologyAxis> axes; final List<_AdminOntologyAxis> axes;
final Set<String> selectedTags; final Set<String> selectedTags;
final MapflowThemeTokens tokens;
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@@ -195,17 +200,17 @@ class _AdminOntologySnowflakePainter extends CustomPainter {
final leafRadius = radius * 0.43; final leafRadius = radius * 0.43;
final baseLine = Paint() final baseLine = Paint()
..color = const Color(0xFFE6DDD2) ..color = tokens.snowflakeLine
..strokeWidth = 1.3 ..strokeWidth = 1.3
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
final selectedLine = Paint() final selectedLine = Paint()
..color = const Color(0xFFE11D48) ..color = tokens.danger
..strokeWidth = 2.2 ..strokeWidth = 2.2
..strokeCap = StrokeCap.round ..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
final node = Paint()..color = const Color(0xFFDED3C7); final node = Paint()..color = tokens.snowflakeNode;
final selectedNode = Paint()..color = const Color(0xFFE11D48); final selectedNode = Paint()..color = tokens.danger;
final centerNode = Paint()..color = const Color(0xFF241B18); final centerNode = Paint()..color = tokens.snowflakeCenter;
canvas.drawCircle(center, 5, centerNode); canvas.drawCircle(center, 5, centerNode);
_drawLabel(canvas, size, center + const Offset(0, 14), 'место', true); _drawLabel(canvas, size, center + const Offset(0, 14), 'место', true);
@@ -276,7 +281,7 @@ class _AdminOntologySnowflakePainter extends CustomPainter {
text: TextSpan( text: TextSpan(
text: label, text: label,
style: TextStyle( style: TextStyle(
color: selected ? const Color(0xFFE11D48) : const Color(0xFF746A60), color: selected ? tokens.danger : tokens.snowflakeLabel,
fontSize: fontSize, fontSize: fontSize,
fontWeight: selected ? FontWeight.w900 : FontWeight.w700, fontWeight: selected ? FontWeight.w900 : FontWeight.w700,
height: 1, height: 1,
@@ -298,7 +303,8 @@ class _AdminOntologySnowflakePainter extends CustomPainter {
@override @override
bool shouldRepaint(covariant _AdminOntologySnowflakePainter oldDelegate) { bool shouldRepaint(covariant _AdminOntologySnowflakePainter oldDelegate) {
return oldDelegate.selectedTags != selectedTags; return oldDelegate.selectedTags != selectedTags ||
oldDelegate.tokens != tokens;
} }
} }
@@ -557,8 +557,8 @@ class _TraitBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = context.read<PlaceCubit>();
final tokens = context.mapflowTokens; final tokens = context.mapflowTokens;
final controller = context.read<PlaceCubit>();
return SizedBox( return SizedBox(
height: 54, height: 54,
@@ -801,6 +801,7 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
final controller = context.read<PlaceCubit>(); final controller = context.read<PlaceCubit>();
final informationProgress = (_informationUnits / _minimumInformationUnits) final informationProgress = (_informationUnits / _minimumInformationUnits)
.clamp(0.0, 1.0); .clamp(0.0, 1.0);
@@ -854,7 +855,7 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
}; };
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF05030B), backgroundColor: tokens.darkSurface,
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
@@ -893,6 +894,8 @@ class _IntroStep extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
return _StepLayout( return _StepLayout(
body: Column( body: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -902,17 +905,17 @@ class _IntroStep extends StatelessWidget {
width: 70, width: 70,
height: 70, height: 70,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE11D48), color: tokens.danger,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(tokens.panelRadius),
), ),
child: const Icon(Icons.graphic_eq, color: Colors.white, size: 38), child: Icon(Icons.graphic_eq, color: tokens.onDark, size: 38),
), ),
const SizedBox(height: 22), const SizedBox(height: 22),
Text( Text(
'Поделись ощущением от места голосом. Нужно быть в заведении или не дальше 50 м. Аудио обработаем и удалим.', 'Поделись ощущением от места голосом. Нужно быть в заведении или не дальше 50 м. Аудио обработаем и удалим.',
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white, color: tokens.onDark,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
height: 1.18, height: 1.18,
letterSpacing: 0, letterSpacing: 0,
@@ -940,6 +943,8 @@ class _PlaceStep extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
return _StepLayout( return _StepLayout(
body: Column( body: Column(
children: [ children: [
@@ -958,15 +963,15 @@ class _PlaceStep extends StatelessWidget {
future: placesFuture, future: placesFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) { if (snapshot.connectionState != ConnectionState.done) {
return const Center( return Center(
child: CircularProgressIndicator(color: Color(0xFFFF2D75)), child: CircularProgressIndicator(color: tokens.voiceAccent),
); );
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return const Center( return Center(
child: Icon( child: Icon(
Icons.error_outline, Icons.error_outline,
color: Colors.white, color: tokens.onDark,
size: 42, size: 42,
), ),
); );
@@ -978,15 +983,15 @@ class _PlaceStep extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon( Icon(
Icons.location_off_outlined, Icons.location_off_outlined,
color: Colors.white, color: tokens.onDark,
size: 42, size: 42,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'Нет мест в $radiusMetersм', 'Нет мест в $radiusMetersм',
style: const TextStyle(color: Colors.white), style: TextStyle(color: tokens.onDark),
), ),
], ],
), ),
@@ -1028,13 +1033,14 @@ class _NearbyPlaceCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final primaryType = _formatType(place.googlePrimaryType); final primaryType = _formatType(place.googlePrimaryType);
final tokens = context.mapflowTokens;
return Material( return Material(
color: const Color(0xFF15111D), color: tokens.darkSurfaceAlt,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(tokens.panelRadius),
child: InkWell( child: InkWell(
onTap: disabled ? null : onTap, onTap: disabled ? null : onTap,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(tokens.panelRadius),
child: Padding( child: Padding(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
child: Column( child: Column(
@@ -1043,15 +1049,18 @@ class _NearbyPlaceCard extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Icon(Icons.place_outlined, color: Colors.white70), Icon(
Icons.place_outlined,
color: tokens.onDark.withValues(alpha: 0.70),
),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
place.name, place.name,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: tokens.onDark,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
height: 1.1, height: 1.1,
), ),
@@ -1090,19 +1099,23 @@ class _PlaceTypeChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: primary color: primary
? const Color(0xFFFF2D75) ? tokens.voiceAccent
: Colors.white.withValues(alpha: 0.12), : tokens.onDark.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(tokens.panelRadius),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
color: primary ? Colors.white : const Color(0xFFECE7F0), color: primary
? tokens.onDark
: tokens.onDark.withValues(alpha: 0.92),
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
height: 1, height: 1,
@@ -1140,6 +1153,7 @@ class _VoiceStep extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
final canFinish = canContinue; final canFinish = canContinue;
return Column( return Column(
@@ -1158,11 +1172,11 @@ class _VoiceStep extends StatelessWidget {
], ],
Expanded(child: _VoiceProgressGrid(progress: informationProgress)), Expanded(child: _VoiceProgressGrid(progress: informationProgress)),
if (!micAllowed) if (!micAllowed)
const Padding( Padding(
padding: EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
child: Icon( child: Icon(
Icons.mic_off_outlined, Icons.mic_off_outlined,
color: Color(0xFFFF7A90), color: tokens.voiceAccentSoft,
size: 22, size: 22,
), ),
), ),
@@ -1249,6 +1263,8 @@ class _VoiceProgressCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
return AnimatedContainer( return AnimatedContainer(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
@@ -1256,13 +1272,13 @@ class _VoiceProgressCell extends StatelessWidget {
height: size, height: size,
decoration: BoxDecoration( decoration: BoxDecoration(
color: filled color: filled
? const Color(0xFFFF2D75) ? tokens.voiceAccent
: Colors.white.withValues(alpha: 0.11), : tokens.onDark.withValues(alpha: 0.11),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
boxShadow: filled boxShadow: filled
? [ ? [
BoxShadow( BoxShadow(
color: const Color(0xFFFF2D75).withValues(alpha: 0.34), color: tokens.voiceAccent.withValues(alpha: 0.34),
blurRadius: 12, blurRadius: 12,
spreadRadius: 1, spreadRadius: 1,
), ),
@@ -1332,6 +1348,8 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
return SizedBox( return SizedBox(
width: 280, width: 280,
height: 280, height: 280,
@@ -1355,7 +1373,7 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton>
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: const Color(0xFFFF2D75).withValues( color: tokens.voiceAccent.withValues(
alpha: widget.isRecording alpha: widget.isRecording
? ((0.06 + level * 0.26) * ? ((0.06 + level * 0.26) *
(1 - ((pulse + offset) % 1))) (1 - ((pulse + offset) % 1)))
@@ -1373,8 +1391,8 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton>
value: widget.progress, value: widget.progress,
strokeWidth: 8, strokeWidth: 8,
strokeCap: StrokeCap.round, strokeCap: StrokeCap.round,
color: const Color(0xFFFF2D75), color: tokens.voiceAccent,
backgroundColor: Colors.white.withValues(alpha: 0.12), backgroundColor: tokens.onDark.withValues(alpha: 0.12),
), ),
), ),
child!, child!,
@@ -1387,10 +1405,10 @@ class _VoiceRecordButtonState extends State<_VoiceRecordButton>
child: FilledButton( child: FilledButton(
onPressed: widget.enabled ? widget.onPressed : null, onPressed: widget.enabled ? widget.onPressed : null,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: tokens.onDark,
foregroundColor: const Color(0xFF090613), foregroundColor: tokens.onVoiceControl,
disabledBackgroundColor: Colors.white.withValues(alpha: 0.28), disabledBackgroundColor: tokens.onDark.withValues(alpha: 0.28),
disabledForegroundColor: Colors.white.withValues(alpha: 0.52), disabledForegroundColor: tokens.onDark.withValues(alpha: 0.52),
shape: const CircleBorder(), shape: const CircleBorder(),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
elevation: 0, elevation: 0,
@@ -1442,6 +1460,8 @@ class _StoryProgress extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = context.mapflowTokens;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
@@ -1455,11 +1475,11 @@ class _StoryProgress extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: index <= step color: index <= step
? (dark ? (dark
? const Color(0xFFFF2D75) ? tokens.voiceAccent
: Theme.of(context).colorScheme.primary) : Theme.of(context).colorScheme.primary)
: (dark : (dark
? Colors.white.withValues(alpha: 0.16) ? tokens.onDark.withValues(alpha: 0.16)
: const Color(0xFFE0D8CA)), : tokens.mapPanelBorder),
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
), ),
), ),
@@ -52,12 +52,12 @@ class PlacePhotoCard extends StatelessWidget {
); );
}, },
), ),
const DecoratedBox( DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Colors.transparent, Color(0xCC000000)], colors: [Colors.transparent, tokens.imageScrim],
), ),
), ),
), ),