refactoring
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/// UnionFlow Primary Button - Bouton principal
|
||||
///
|
||||
/// Bouton primaire avec la couleur Bleu Roi (#4169E1)
|
||||
///
|
||||
/// Bouton primaire Vert Forêt (#2E7D32)
|
||||
/// Utilisé pour les actions principales (connexion, enregistrer, valider, etc.)
|
||||
library uf_primary_button;
|
||||
|
||||
@@ -59,22 +59,22 @@ class UFPrimaryButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: isFullWidth ? double.infinity : null,
|
||||
height: height ?? SpacingTokens.buttonHeightLarge,
|
||||
height: height ?? SpacingTokens.buttonHeightMedium,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? AppColors.primaryGreen,
|
||||
foregroundColor: textColor ?? Colors.white,
|
||||
backgroundColor: backgroundColor ?? AppColors.primaryGreen,
|
||||
foregroundColor: textColor ?? Colors.white,
|
||||
disabledBackgroundColor: (backgroundColor ?? AppColors.primaryGreen).withOpacity(0.5),
|
||||
disabledForegroundColor: (textColor ?? Colors.white).withOpacity(0.7),
|
||||
elevation: SpacingTokens.elevationSm,
|
||||
shadowColor: AppColors.darkBorder.withOpacity(0.1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.buttonPaddingHorizontal,
|
||||
vertical: SpacingTokens.buttonPaddingVertical,
|
||||
vertical: 10,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// UnionFlow Secondary Button - Bouton secondaire
|
||||
///
|
||||
/// Bouton secondaire avec la couleur Indigo (#6366F1)
|
||||
///
|
||||
/// Bouton secondaire Vert Menthe (#4CAF50)
|
||||
/// Utilisé pour les actions secondaires (annuler, retour, etc.)
|
||||
library uf_secondary_button;
|
||||
|
||||
@@ -30,22 +30,22 @@ class UFSecondaryButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: isFullWidth ? double.infinity : null,
|
||||
height: height ?? SpacingTokens.buttonHeightLarge,
|
||||
height: height ?? SpacingTokens.buttonHeightMedium,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.brandGreen,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.brandGreen,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: AppColors.brandGreen.withOpacity(0.5),
|
||||
disabledForegroundColor: Colors.white.withOpacity(0.7),
|
||||
elevation: SpacingTokens.elevationSm,
|
||||
shadowColor: AppColors.darkBorder.withOpacity(0.1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.buttonPaddingHorizontal,
|
||||
vertical: SpacingTokens.buttonPaddingVertical,
|
||||
vertical: 10,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
|
||||
@@ -83,9 +83,9 @@ class UFCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.cardPadding);
|
||||
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.lg);
|
||||
final effectiveMargin = margin ?? EdgeInsets.zero;
|
||||
final effectiveBorderRadius = borderRadius ?? SpacingTokens.radiusLg;
|
||||
final effectiveBorderRadius = borderRadius ?? SpacingTokens.radiusMd;
|
||||
|
||||
Widget content = Container(
|
||||
padding: effectivePadding,
|
||||
@@ -114,15 +114,6 @@ class UFCard extends StatelessWidget {
|
||||
return BoxDecoration(
|
||||
color: color ?? AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
boxShadow: elevation != null
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.darkBorder.withOpacity(0.1),
|
||||
blurRadius: elevation!,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: ShadowTokens.sm,
|
||||
);
|
||||
|
||||
case UFCardStyle.outlined:
|
||||
|
||||
@@ -49,15 +49,15 @@ class UFInfoCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.xl);
|
||||
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.lg);
|
||||
|
||||
return Container(
|
||||
padding: effectivePadding,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
boxShadow: ShadowTokens.sm,
|
||||
color: isDark ? AppColors.darkSurface : Colors.white,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -65,20 +65,20 @@ class UFInfoCard extends StatelessWidget {
|
||||
// Header avec titre et trailing
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: effectiveIconColor, size: 20),
|
||||
Icon(icon, color: effectiveIconColor, size: 16),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
// Contenu
|
||||
child,
|
||||
],
|
||||
|
||||
@@ -45,7 +45,7 @@ class UFMetricCard extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
@@ -55,21 +55,22 @@ class UFStatCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
final effectiveIconBgColor = iconBackgroundColor ??
|
||||
final effectiveIconBgColor = iconBackgroundColor ??
|
||||
effectiveIconColor.withOpacity(0.1);
|
||||
|
||||
return Card(
|
||||
elevation: SpacingTokens.elevationSm,
|
||||
shadowColor: AppColors.darkBorder.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(SpacingTokens.cardPadding),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -79,55 +80,55 @@ class UFStatCard extends StatelessWidget {
|
||||
children: [
|
||||
// Icône avec background coloré
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveIconBgColor,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: effectiveIconColor,
|
||||
size: 24,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Flèche si cliquable
|
||||
if (onTap != null)
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
|
||||
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
|
||||
// Titre
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
|
||||
|
||||
// Valeur
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Sous-titre optionnel
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
107
lib/shared/design_system/components/dashboard_activity_row.dart
Normal file
107
lib/shared/design_system/components/dashboard_activity_row.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Ligne d'activité récente — style fintech compact identique au super_admin
|
||||
/// Icône dans carré arrondi 28×28 + titre + description + timestamp
|
||||
class DashboardActivityRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final String timeAgo;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const DashboardActivityRow({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.timeAgo,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: UnionFlowColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(icon, size: 14, color: color),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
timeAgo,
|
||||
style: const TextStyle(fontSize: 10, color: UnionFlowColors.textTertiary),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Icône selon le type d'activité
|
||||
static IconData iconFor(String type) {
|
||||
switch (type) {
|
||||
case 'member':
|
||||
return Icons.person_add_rounded;
|
||||
case 'event':
|
||||
return Icons.event_rounded;
|
||||
case 'contribution':
|
||||
return Icons.payments_rounded;
|
||||
default:
|
||||
return Icons.circle_notifications_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
/// Couleur selon le type d'activité
|
||||
static Color colorFor(String type) {
|
||||
switch (type) {
|
||||
case 'member':
|
||||
return UnionFlowColors.unionGreen;
|
||||
case 'event':
|
||||
return UnionFlowColors.info;
|
||||
case 'contribution':
|
||||
return UnionFlowColors.gold;
|
||||
default:
|
||||
return UnionFlowColors.textSecondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
lib/shared/design_system/components/dashboard_event_row.dart
Normal file
88
lib/shared/design_system/components/dashboard_event_row.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Ligne d'événement à venir — style fintech avec bordure gauche verte
|
||||
/// Titre + date + countdown optionnel + participants optionnel
|
||||
class DashboardEventRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String date;
|
||||
final String? daysUntil;
|
||||
final String? participants;
|
||||
|
||||
const DashboardEventRow({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.date,
|
||||
this.daysUntil,
|
||||
this.participants,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: const Border(
|
||||
left: BorderSide(color: UnionFlowColors.unionGreen, width: 3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
date,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (daysUntil != null || participants != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (daysUntil != null)
|
||||
Text(
|
||||
daysUntil!,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
if (participants != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
participants!,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,9 @@ class UFDropdownTile<T> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final effectiveBgColor = backgroundColor ??
|
||||
(isDark ? AppColors.darkSurface : AppColors.lightSurface);
|
||||
final effectiveItemBuilder = itemBuilder ?? (item) => item.toString();
|
||||
|
||||
return Container(
|
||||
@@ -65,16 +67,16 @@ class UFDropdownTile<T> extends StatelessWidget {
|
||||
title,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: isDark ? AppColors.darkBackground : Colors.white,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
border: Border.all(color: AppColors.lightBorder),
|
||||
border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<T>(
|
||||
|
||||
@@ -44,7 +44,9 @@ class UFSwitchTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final effectiveBgColor = backgroundColor ??
|
||||
(isDark ? AppColors.darkSurface : AppColors.lightSurface);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
|
||||
@@ -63,13 +65,13 @@ class UFSwitchTile extends StatelessWidget {
|
||||
title,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -65,7 +65,7 @@ class UFContainer extends StatelessWidget {
|
||||
this.gradient,
|
||||
this.border,
|
||||
this.boxShadow,
|
||||
}) : borderRadius = SpacingTokens.radiusLg;
|
||||
}) : borderRadius = SpacingTokens.radiusMd;
|
||||
|
||||
/// Container très arrondi
|
||||
const UFContainer.extraRounded({
|
||||
@@ -81,7 +81,7 @@ class UFContainer extends StatelessWidget {
|
||||
this.gradient,
|
||||
this.border,
|
||||
this.boxShadow,
|
||||
}) : borderRadius = SpacingTokens.radiusXl;
|
||||
}) : borderRadius = SpacingTokens.radiusLg;
|
||||
|
||||
/// Container avec ombre
|
||||
UFContainer.elevated({
|
||||
@@ -96,8 +96,8 @@ class UFContainer extends StatelessWidget {
|
||||
this.constraints,
|
||||
this.gradient,
|
||||
this.border,
|
||||
}) : borderRadius = SpacingTokens.radiusLg,
|
||||
boxShadow = ShadowTokens.sm;
|
||||
}) : borderRadius = SpacingTokens.radiusMd,
|
||||
boxShadow = null;
|
||||
|
||||
/// Container circulaire
|
||||
const UFContainer.circular({
|
||||
|
||||
@@ -28,13 +28,12 @@ class UFHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.xl),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primaryGreen, AppColors.brandGreenLight],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
boxShadow: ShadowTokens.primary,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -43,12 +42,12 @@ class UFHeader extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.lg),
|
||||
@@ -95,7 +94,7 @@ class UFHeader extends StatelessWidget {
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: onNotificationTap,
|
||||
@@ -111,7 +110,7 @@ class UFHeader extends StatelessWidget {
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: onSettingsTap,
|
||||
|
||||
@@ -34,6 +34,7 @@ class UFPageHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
|
||||
return Column(
|
||||
@@ -59,30 +60,30 @@ class UFPageHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
|
||||
|
||||
// Titre
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Actions
|
||||
if (actions != null) ...actions!,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Divider optionnel
|
||||
if (showDivider)
|
||||
const Divider(
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: AppColors.lightBorder,
|
||||
color: isDark ? AppColors.darkBorder : AppColors.lightBorder,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -110,6 +111,7 @@ class UFPageHeaderWithStats extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
|
||||
return Column(
|
||||
@@ -138,18 +140,18 @@ class UFPageHeaderWithStats extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
|
||||
|
||||
// Titre
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Actions
|
||||
if (actions != null) ...actions!,
|
||||
],
|
||||
@@ -172,7 +174,7 @@ class UFPageHeaderWithStats extends StatelessWidget {
|
||||
padding: EdgeInsets.only(
|
||||
right: isLast ? 0 : SpacingTokens.sm,
|
||||
),
|
||||
child: _buildStatItem(stat),
|
||||
child: _buildStatItem(stat, isDark),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -180,16 +182,16 @@ class UFPageHeaderWithStats extends StatelessWidget {
|
||||
),
|
||||
|
||||
// Divider
|
||||
const Divider(
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: AppColors.lightBorder,
|
||||
color: isDark ? AppColors.darkBorder : AppColors.lightBorder,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(UFHeaderStat stat) {
|
||||
Widget _buildStatItem(UFHeaderStat stat, bool isDark) {
|
||||
final effectiveColor = stat.color ?? AppColors.primaryGreen;
|
||||
return UFContainer.rounded(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -211,7 +213,7 @@ class UFPageHeaderWithStats extends StatelessWidget {
|
||||
Text(
|
||||
stat.label,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
39
lib/shared/design_system/components/uf_section_header.dart
Normal file
39
lib/shared/design_system/components/uf_section_header.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// En-tête de section dashboard — titre 13px w700 avec trailing optionnel
|
||||
/// Style de référence : super_admin_dashboard._buildSectionHeader
|
||||
class UFSectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
/// Texte secondaire affiché à droite : "· trailing"
|
||||
final String? trailing;
|
||||
|
||||
const UFSectionHeader(this.title, {this.trailing, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
if (trailing != null && trailing!.isNotEmpty) ...[
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'· $trailing',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Bouton d'action rapide UnionFlow
|
||||
/// Style fintech : fond blanc, icône + texte colorés, bordure grise légère
|
||||
/// Copie exacte du style _buildActionCell du super_admin_dashboard
|
||||
class UnionActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
@@ -20,36 +22,36 @@ class UnionActionButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
// backgroundColor sert d'accent (icône + texte), iconColor prend la priorité
|
||||
final accentColor = iconColor ?? backgroundColor ?? UnionFlowColors.unionGreen;
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? UnionFlowColors.unionGreenPale,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: (backgroundColor ?? UnionFlowColors.unionGreenPale)
|
||||
.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: UnionFlowColors.border),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 28,
|
||||
color: iconColor ?? UnionFlowColors.unionGreen,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
Icon(icon, size: 16, color: accentColor),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: accentColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -73,7 +75,7 @@ class UnionActionGrid extends StatelessWidget {
|
||||
children: [
|
||||
for (int i = 0; i < actions.length; i++) ...[
|
||||
Expanded(child: actions[i]),
|
||||
if (i < actions.length - 1) const SizedBox(width: 12),
|
||||
if (i < actions.length - 1) const SizedBox(width: 10),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
@@ -22,75 +22,87 @@ class UnionBalanceCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
// Bordure dorée subtile en haut
|
||||
border: const Border(
|
||||
top: BorderSide(
|
||||
color: UnionFlowColors.gold,
|
||||
width: 3,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
border: Border.all(color: UnionFlowColors.border),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Label
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Montant principal
|
||||
Text(
|
||||
amount,
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
|
||||
// Trend (optionnel)
|
||||
if (trend != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isTrendPositive == true
|
||||
? Icons.trending_up
|
||||
: Icons.trending_down,
|
||||
size: 16,
|
||||
color: isTrendPositive == true
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
trend!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isTrendPositive == true
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.error,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(height: 2, color: UnionFlowColors.gold),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
amount,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
height: 1,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (trend != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isTrendPositive == true
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
size: 11,
|
||||
color: isTrendPositive == true
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.error,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
trend!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isTrendPositive == true
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
'ce mois',
|
||||
style: TextStyle(fontSize: 9, color: UnionFlowColors.textTertiary),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -26,25 +26,18 @@ class UnionGlassCard extends StatelessWidget {
|
||||
child: Container(
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 16),
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 10),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UnionFlowColors.unionGreen.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 16),
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 10),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
padding: padding ?? const EdgeInsets.all(20),
|
||||
padding: padding ?? const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
|
||||
@@ -77,11 +77,10 @@ class UnionLineChart extends StatelessWidget {
|
||||
final effectiveGradientEnd = gradientEndColor ?? UnionFlowColors.unionGreen.withOpacity(0.0);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -105,7 +104,7 @@ class UnionLineChart extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Chart
|
||||
SizedBox(
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Graphique circulaire UnionFlow - Pour afficher des répartitions
|
||||
/// Graphique circulaire UnionFlow — petit donut compact avec légende latérale
|
||||
/// Layout horizontal : donut 80×80 à gauche + légende à droite
|
||||
/// Style uniforme avec les autres cards du design system (border + borderRadius)
|
||||
class UnionPieChart extends StatelessWidget {
|
||||
final List<PieChartSectionData> sections;
|
||||
final String title;
|
||||
@@ -19,47 +21,110 @@ class UnionPieChart extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Forcer compact : radius 14, pas de titre dans le donut
|
||||
final compactSections = sections
|
||||
.map((s) => s.copyWith(radius: 14, showTitle: false))
|
||||
.toList();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: UnionFlowColors.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Chart
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: centerSpaceRadius ?? 50,
|
||||
sections: sections,
|
||||
// Donut + légende côte à côte
|
||||
Row(
|
||||
children: [
|
||||
// Petit donut
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: centerSpaceRadius ?? 22,
|
||||
sections: compactSections,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// Légende
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: sections.map((s) {
|
||||
// Le title de la section peut contenir '\n' ex: '50%\nActifs'
|
||||
final parts = s.title.split('\n');
|
||||
final pct = parts.isNotEmpty ? parts[0] : '';
|
||||
final label = parts.length > 1 ? parts[1] : s.title;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: s.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
pct,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: s.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -79,8 +144,9 @@ class UnionPieChartSection {
|
||||
return PieChartSectionData(
|
||||
color: color,
|
||||
value: value,
|
||||
title: showTitle ? title : '',
|
||||
title: title,
|
||||
radius: radius,
|
||||
showTitle: showTitle,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -25,11 +25,10 @@ class UnionProgressCard extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -43,7 +42,7 @@ class UnionProgressCard extends StatelessWidget {
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Progress bar
|
||||
Stack(
|
||||
@@ -69,13 +68,6 @@ class UnionProgressCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: effectiveColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Widget de statistique compacte avec icône et tendance
|
||||
/// Widget de statistique compacte — style identique à _buildKpiCell du super admin
|
||||
/// fond blanc, bordure gauche colorée, icône + valeur + label
|
||||
/// [compact] réduit le padding vertical pour les grilles très plates
|
||||
class UnionStatWidget extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
@@ -10,6 +12,9 @@ class UnionStatWidget extends StatelessWidget {
|
||||
final String? trend;
|
||||
final bool? isTrendUp;
|
||||
|
||||
/// Mode ultra-compact : padding vertical réduit, espacement minimal
|
||||
final bool compact;
|
||||
|
||||
const UnionStatWidget({
|
||||
super.key,
|
||||
required this.label,
|
||||
@@ -18,86 +23,65 @@ class UnionStatWidget extends StatelessWidget {
|
||||
required this.color,
|
||||
this.trend,
|
||||
this.isTrendUp,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EdgeInsets pad = compact
|
||||
? const EdgeInsets.symmetric(horizontal: 8, vertical: 5)
|
||||
: const EdgeInsets.all(6);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: pad,
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: color,
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border(left: BorderSide(color: color, width: 3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, size: 20, color: color),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Value
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Label
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
// Trend
|
||||
if (trend != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isTrendUp == true
|
||||
? Icons.trending_up
|
||||
: Icons.trending_down,
|
||||
size: 14,
|
||||
color: isTrendUp == true
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 13, color: color),
|
||||
if (trend != null) ...[
|
||||
const Spacer(),
|
||||
Text(
|
||||
trend!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isTrendUp == true
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.error,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: compact ? 2 : 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: color,
|
||||
letterSpacing: -0.3,
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: compact ? 1 : 2),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -153,11 +153,10 @@ class UnionTransactionCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -35,14 +35,7 @@ class UnionUnifiedAccountCard extends StatelessWidget {
|
||||
UnionFlowColors.unionGreen.withOpacity(0.85),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UnionFlowColors.unionGreen.withOpacity(0.35),
|
||||
offset: const Offset(0, 10),
|
||||
blurRadius: 20,
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -58,7 +51,7 @@ class UnionUnifiedAccountCard extends StatelessWidget {
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -95,7 +88,7 @@ class UnionUnifiedAccountCard extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.white30),
|
||||
),
|
||||
child: Text(
|
||||
@@ -111,8 +104,8 @@ class UnionUnifiedAccountCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Solde Total Disponible
|
||||
const Text(
|
||||
'Solde Total Disponible',
|
||||
@@ -129,15 +122,15 @@ class UnionUnifiedAccountCard extends StatelessWidget {
|
||||
soldeTotal,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 36,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Grille de détails
|
||||
Row(
|
||||
children: [
|
||||
@@ -147,8 +140,8 @@ class UnionUnifiedAccountCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Barre d'engagement
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
120
lib/shared/design_system/components/user_identity_card.dart
Normal file
120
lib/shared/design_system/components/user_identity_card.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Carte identité utilisateur — header uniforme pour tous les dashboards
|
||||
/// Gradient coloré + initiales + nom + sous-titre + badge rôle
|
||||
class UserIdentityCard extends StatelessWidget {
|
||||
final String initials;
|
||||
final String name;
|
||||
final String subtitle;
|
||||
final String badgeLabel;
|
||||
final Gradient gradient;
|
||||
final Color accentColor;
|
||||
|
||||
/// true = fond clair → texte sombre, badge avec fond coloré
|
||||
/// false (défaut) = fond sombre → texte blanc, badge blanc + texte coloré
|
||||
final bool lightBackground;
|
||||
|
||||
/// Afficher la bordure supérieure colorée (accentColor)
|
||||
final bool showTopBorder;
|
||||
|
||||
const UserIdentityCard({
|
||||
super.key,
|
||||
required this.initials,
|
||||
required this.name,
|
||||
required this.subtitle,
|
||||
required this.badgeLabel,
|
||||
required this.gradient,
|
||||
required this.accentColor,
|
||||
this.lightBackground = false,
|
||||
this.showTopBorder = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor =
|
||||
lightBackground ? UnionFlowColors.textPrimary : Colors.white;
|
||||
final subtitleColor = lightBackground
|
||||
? UnionFlowColors.textSecondary
|
||||
: Colors.white.withOpacity(0.85);
|
||||
final avatarBg = lightBackground
|
||||
? accentColor.withOpacity(0.15)
|
||||
: Colors.white.withOpacity(0.25);
|
||||
final avatarTextColor = lightBackground ? accentColor : Colors.white;
|
||||
final badgeBg = lightBackground ? accentColor : Colors.white;
|
||||
final badgeTextColor = lightBackground ? Colors.white : accentColor;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: showTopBorder
|
||||
? Border(top: BorderSide(color: accentColor, width: 3))
|
||||
: null,
|
||||
boxShadow: showTopBorder
|
||||
? [
|
||||
BoxShadow(
|
||||
color: accentColor.withOpacity(0.25),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
)
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: avatarBg,
|
||||
child: Text(
|
||||
initials,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: avatarTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(fontSize: 11, color: subtitleColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
badgeLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: badgeTextColor,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class AppThemeSophisticated {
|
||||
// Couleurs principales
|
||||
colorScheme: _lightColorScheme,
|
||||
|
||||
// Typographie
|
||||
// Typographie (Playfair Display display + Inter body)
|
||||
textTheme: _textTheme,
|
||||
|
||||
// Configuration de l'AppBar
|
||||
@@ -85,19 +85,34 @@ class AppThemeSophisticated {
|
||||
);
|
||||
}
|
||||
|
||||
/// Thème sombre (suit le système ou sélection manuelle)
|
||||
/// Thème sombre — Vert Ardoise (#1A2E1A)
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.dark(
|
||||
primary: ColorTokens.primary,
|
||||
onPrimary: Colors.white,
|
||||
surface: const Color(0xFF121212),
|
||||
onSurface: Colors.white,
|
||||
error: ColorTokens.error,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: Color(0xFF4CAF50), // Vert clair sur fond sombre
|
||||
onPrimary: Color(0xFF003908),
|
||||
primaryContainer: Color(0xFF1E3A1E),
|
||||
onPrimaryContainer: Color(0xFFB9F0B9),
|
||||
secondary: Color(0xFFA5D6A7),
|
||||
onSecondary: Color(0xFF002106),
|
||||
surface: Color(0xFF1A2E1A), // Vert ardoise
|
||||
onSurface: Color(0xFFE0F2E0),
|
||||
surfaceContainerHighest: Color(0xFF243824),
|
||||
onSurfaceVariant: Color(0xFF90B890),
|
||||
error: Color(0xFFEF4444),
|
||||
onError: Colors.white,
|
||||
outline: Color(0xFF3A5E3A),
|
||||
shadow: Color(0xFF0F1A0F),
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF0F1A0F),
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Color(0xFFE0F2E0),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,27 +169,27 @@ class AppThemeSophisticated {
|
||||
// THÈME TYPOGRAPHIQUE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static const TextTheme _textTheme = TextTheme(
|
||||
// Display styles
|
||||
static TextTheme get _textTheme => TextTheme(
|
||||
// Display styles — Playfair Display (GoogleFonts, non-const)
|
||||
displayLarge: TypographyTokens.displayLarge,
|
||||
displayMedium: TypographyTokens.displayMedium,
|
||||
displaySmall: TypographyTokens.displaySmall,
|
||||
|
||||
|
||||
// Headline styles
|
||||
headlineLarge: TypographyTokens.headlineLarge,
|
||||
headlineMedium: TypographyTokens.headlineMedium,
|
||||
headlineSmall: TypographyTokens.headlineSmall,
|
||||
|
||||
|
||||
// Title styles
|
||||
titleLarge: TypographyTokens.titleLarge,
|
||||
titleMedium: TypographyTokens.titleMedium,
|
||||
titleSmall: TypographyTokens.titleSmall,
|
||||
|
||||
|
||||
// Label styles
|
||||
labelLarge: TypographyTokens.labelLarge,
|
||||
labelMedium: TypographyTokens.labelMedium,
|
||||
labelSmall: TypographyTokens.labelSmall,
|
||||
|
||||
|
||||
// Body styles
|
||||
bodyLarge: TypographyTokens.bodyLarge,
|
||||
bodyMedium: TypographyTokens.bodyMedium,
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// UnionFlow Mobile App - Couleurs Globales (Strict DRY)
|
||||
/// Palette principale: Vert, Blanc (Jour), Noir (Nuit).
|
||||
/// UnionFlow Mobile App - Couleurs Globales (DRY)
|
||||
/// Palette : Vert Forêt (Jour) / Vert Ardoise (Nuit)
|
||||
class AppColors {
|
||||
// --- Branding ---
|
||||
static const Color primaryGreen = Color(0xFF17BF63); // Vert vibrant style social (Fb/Tw) - Corrigé.
|
||||
static const Color brandGreen = Color(0xFF2E7D32); // Vert professionnel et lisible
|
||||
static const Color primaryGreen = Color(0xFF2E7D32); // Vert forêt professionnel
|
||||
static const Color brandGreen = Color(0xFF1B5E20); // Vert foncé / gradient top
|
||||
static const Color brandGreenLight = Color(0xFF4CAF50); // Vert d'accentuation
|
||||
|
||||
static const Color brandMint = Color(0xFFA5D6A7); // Vert menthe / secondaire
|
||||
|
||||
// --- Mode Jour (Light) ---
|
||||
static const Color lightBackground = Color(0xFFFFFFFF); // Blanc pur
|
||||
static const Color lightSurface = Color(0xFFF5F8FA); // Gris extrêmement léger pour séparer les cards
|
||||
static const Color lightBorder = Color(0xFFE1E8ED); // Bordures style Twitter
|
||||
|
||||
// --- Mode Nuit (Dark OLED) ---
|
||||
static const Color darkBackground = Color(0xFF000000); // Noir pur pour OLED
|
||||
static const Color darkSurface = Color(0xFF15202B); // Gris sombre typique Twitter/Fb Dark
|
||||
static const Color darkBorder = Color(0xFF38444D); // Bordure discrète sombre
|
||||
static const Color lightBackground = Color(0xFFF1F8E9); // Teinte verte très légère
|
||||
static const Color lightSurface = Color(0xFFFFFFFF); // Blanc pur pour les cartes
|
||||
static const Color lightBorder = Color(0xFFC8E6C9); // Bordure vert pâle
|
||||
|
||||
// --- Mode Nuit (Dark) ---
|
||||
static const Color darkBackground = Color(0xFF0F1A0F); // Vert noir profond
|
||||
static const Color darkSurface = Color(0xFF1A2E1A); // Vert ardoise
|
||||
static const Color darkBorder = Color(0xFF3A5E3A); // Bordure sombre
|
||||
|
||||
// --- Texte ---
|
||||
static const Color textPrimaryLight = Color(0xFF14171A); // Presque noir
|
||||
static const Color textSecondaryLight = Color(0xFF657786); // Gris texte
|
||||
static const Color textPrimaryDark = Color(0xFFE1E8ED); // Presque blanc
|
||||
static const Color textSecondaryDark = Color(0xFF8899A6); // Gris clair nuit
|
||||
static const Color textPrimaryLight = Color(0xFF1C2B1C); // Vert très foncé / quasi noir
|
||||
static const Color textSecondaryLight = Color(0xFF4E6B4E); // Vert gris moyen
|
||||
static const Color textPrimaryDark = Color(0xFFE0F2E0); // Blanc verdâtre
|
||||
static const Color textSecondaryDark = Color(0xFF90B890); // Vert gris clair
|
||||
|
||||
// --- Sémantique (Succès, Erreur, Info) ---
|
||||
static const Color error = Color(0xFFE0245E); // Rouge vif
|
||||
static const Color success = Color(0xFF17BF63); // Vert validation
|
||||
static const Color warning = Color(0xFFFFAD1F); // Orange
|
||||
static const Color info = Color(0xFF1DA1F2); // Bleu info
|
||||
// --- Sémantique ---
|
||||
static const Color error = Color(0xFFDC2626);
|
||||
static const Color success = Color(0xFF2E7D32);
|
||||
static const Color warning = Color(0xFFF59E0B);
|
||||
static const Color info = Color(0xFF0288D1);
|
||||
|
||||
// --- Utilitaires ---
|
||||
static const Color transparent = Colors.transparent;
|
||||
|
||||
@@ -1,47 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
|
||||
/// UnionFlow Mobile App - Typographie Globale (Ultra Minimaliste)
|
||||
/// RÈGLE : AUCUN gros titre. Tailles limitées entre 10px et 14px pour maximiser l'information.
|
||||
/// UnionFlow Mobile App - Typographie Globale
|
||||
/// Roboto (Google Fonts) — cohérence cross-platform
|
||||
class AppTypography {
|
||||
static const String _fontFamily = 'Roboto'; // Peut être changé pour 'Inter' si ajouté au pubspec.yaml
|
||||
static const String _fontFamily = 'Roboto';
|
||||
|
||||
// --- Titres (Max 14px) ---
|
||||
static const TextStyle headerSmall = TextStyle(
|
||||
// --- Display / Titres principaux (Roboto via GoogleFonts) ---
|
||||
|
||||
static TextStyle get displayLarge => GoogleFonts.roboto(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
static TextStyle get displayMedium => GoogleFonts.roboto(
|
||||
fontSize: 28.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.25,
|
||||
height: 1.25,
|
||||
);
|
||||
|
||||
static TextStyle get displaySmall => GoogleFonts.roboto(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
// --- Titres de sections (Inter SemiBold) ---
|
||||
|
||||
static const TextStyle headerLarge = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w700, // Bold
|
||||
fontSize: 22.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.2,
|
||||
height: 1.27,
|
||||
);
|
||||
|
||||
// --- Corps de texte (Max 12px) ---
|
||||
/// Alias historique — conservé pour compatibilité
|
||||
static const TextStyle headerSmall = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.2,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
static const TextStyle titleMedium = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.1,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
static const TextStyle titleSmall = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.1,
|
||||
height: 1.43,
|
||||
);
|
||||
|
||||
// --- Corps de texte ---
|
||||
|
||||
static const TextStyle bodyLarge = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.3,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
static const TextStyle bodyMedium = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.25,
|
||||
height: 1.43,
|
||||
);
|
||||
|
||||
/// Alias historique — conservé pour compatibilité
|
||||
static const TextStyle bodyTextSmall = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w400, // Regular
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.2,
|
||||
height: 1.4,
|
||||
);
|
||||
|
||||
// --- Boutons et Actions (Max 13px) ---
|
||||
// --- Actions et boutons ---
|
||||
|
||||
static const TextStyle actionText = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w600, // SemiBold
|
||||
fontSize: 15.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.1,
|
||||
);
|
||||
|
||||
// --- Sous-titres, dates, labels (Max 11px) ---
|
||||
static const TextStyle subtitleSmall = TextStyle(
|
||||
static const TextStyle buttonLabel = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w300, // Light
|
||||
letterSpacing: 0.2,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.1,
|
||||
);
|
||||
|
||||
// --- Badges, Piles, Métriques très denses (Max 10px) ---
|
||||
// --- Sous-titres et labels ---
|
||||
|
||||
/// Alias historique — conservé pour compatibilité
|
||||
static const TextStyle subtitleSmall = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.2,
|
||||
height: 1.4,
|
||||
);
|
||||
|
||||
static const TextStyle labelMedium = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.4,
|
||||
height: 1.33,
|
||||
);
|
||||
|
||||
// --- Badges, métriques denses ---
|
||||
|
||||
/// Alias historique — conservé pour compatibilité
|
||||
static const TextStyle badgeText = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 10.0,
|
||||
fontWeight: FontWeight.w500, // Medium
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
);
|
||||
|
||||
static const TextStyle caption = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.4,
|
||||
height: 1.45,
|
||||
);
|
||||
|
||||
// --- Navigation ---
|
||||
|
||||
static const TextStyle navLabel = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
);
|
||||
|
||||
static const TextStyle navLabelSelected = TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.3,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/// Design Tokens - Couleurs UnionFlow
|
||||
///
|
||||
/// Palette de couleurs Bleu Roi + Bleu Pétrole
|
||||
/// Inspirée des tendances UI/UX 2024-2025
|
||||
/// Basée sur les principes de Material Design 3
|
||||
///
|
||||
/// MODE JOUR: Bleu Roi (#4169E1) - Royal Blue
|
||||
/// MODE NUIT: Bleu Pétrole (#2C5F6F) - Petroleum Blue
|
||||
/// Palette Vert Zen / Santé — Professionnelle et apaisante
|
||||
/// MODE JOUR : Vert Forêt (#2E7D32) — Forest Green
|
||||
/// MODE NUIT : Vert Ardoise (#1A2E1A) — Slate Green
|
||||
library color_tokens;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -15,180 +12,176 @@ class ColorTokens {
|
||||
ColorTokens._();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS PRIMAIRES - MODE JOUR (Bleu Roi)
|
||||
// COULEURS PRIMAIRES - MODE JOUR (Vert Forêt)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Couleur primaire principale - Bleu Roi (Royal Blue)
|
||||
static const Color primary = Color(0xFF4169E1); // Bleu roi
|
||||
static const Color primaryLight = Color(0xFF6B8EF5); // Bleu roi clair
|
||||
static const Color primaryDark = Color(0xFF2952C8); // Bleu roi sombre
|
||||
static const Color primaryContainer = Color(0xFFE3ECFF); // Container bleu roi
|
||||
static const Color onPrimary = Color(0xFFFFFFFF); // Texte sur primaire (blanc)
|
||||
static const Color onPrimaryContainer = Color(0xFF001A41); // Texte sur container
|
||||
/// Couleur primaire principale - Vert Forêt
|
||||
static const Color primary = Color(0xFF2E7D32);
|
||||
static const Color primaryLight = Color(0xFF4CAF50);
|
||||
static const Color primaryDark = Color(0xFF1B5E20);
|
||||
static const Color primaryContainer = Color(0xFFE8F5E9);
|
||||
static const Color onPrimary = Color(0xFFFFFFFF);
|
||||
static const Color onPrimaryContainer = Color(0xFF003908);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS PRIMAIRES - MODE NUIT (Bleu Pétrole)
|
||||
// COULEURS PRIMAIRES - MODE NUIT (Vert Ardoise)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Couleur primaire mode nuit - Bleu Pétrole
|
||||
static const Color primaryDarkMode = Color(0xFF2C5F6F); // Bleu pétrole
|
||||
static const Color primaryLightDarkMode = Color(0xFF3D7A8C); // Bleu pétrole clair
|
||||
static const Color primaryDarkDarkMode = Color(0xFF1B4D5C); // Bleu pétrole sombre
|
||||
static const Color primaryContainerDarkMode = Color(0xFF1E3A44); // Container mode nuit
|
||||
static const Color onPrimaryDarkMode = Color(0xFFE5E7EB); // Texte sur primaire (gris clair)
|
||||
static const Color primaryDarkMode = Color(0xFF1A2E1A);
|
||||
static const Color primaryLightDarkMode = Color(0xFF2E4D2E);
|
||||
static const Color primaryDarkDarkMode = Color(0xFF0F1A0F);
|
||||
static const Color primaryContainerDarkMode = Color(0xFF1E3A1E);
|
||||
static const Color onPrimaryDarkMode = Color(0xFFE0F2E0);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS SECONDAIRES - Indigo Moderne
|
||||
// COULEURS SECONDAIRES - Vert Menthe / Sauge
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static const Color secondary = Color(0xFF6366F1); // Indigo moderne
|
||||
static const Color secondaryLight = Color(0xFF8B8FF6); // Indigo clair
|
||||
static const Color secondaryDark = Color(0xFF4F46E5); // Indigo sombre
|
||||
static const Color secondaryContainer = Color(0xFFE0E7FF); // Container indigo
|
||||
static const Color secondary = Color(0xFF66BB6A);
|
||||
static const Color secondaryLight = Color(0xFFA5D6A7);
|
||||
static const Color secondaryDark = Color(0xFF388E3C);
|
||||
static const Color secondaryContainer = Color(0xFFC8E6C9);
|
||||
static const Color onSecondary = Color(0xFFFFFFFF);
|
||||
static const Color onSecondaryContainer = Color(0xFF1E1B3A);
|
||||
static const Color onSecondaryContainer = Color(0xFF002106);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS TERTIAIRES - Vert Émeraude
|
||||
// COULEURS TERTIAIRES - Vert Lime / Accent
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static const Color tertiary = Color(0xFF10B981); // Vert émeraude
|
||||
static const Color tertiaryLight = Color(0xFF34D399); // Vert clair
|
||||
static const Color tertiaryDark = Color(0xFF059669); // Vert sombre
|
||||
static const Color tertiaryContainer = Color(0xFFD1FAE5); // Container vert
|
||||
static const Color tertiary = Color(0xFF8BC34A);
|
||||
static const Color tertiaryLight = Color(0xFFAED581);
|
||||
static const Color tertiaryDark = Color(0xFF558B2F);
|
||||
static const Color tertiaryContainer = Color(0xFFDCEDC8);
|
||||
static const Color onTertiary = Color(0xFFFFFFFF);
|
||||
static const Color onTertiaryContainer = Color(0xFF002114);
|
||||
static const Color onTertiaryContainer = Color(0xFF1B3A00);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS NEUTRES - MODE JOUR
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static const Color surface = Color(0xFFFFFFFF); // Surface principale (blanc)
|
||||
static const Color surfaceVariant = Color(0xFFF8F9FA); // Surface variante (gris très clair)
|
||||
static const Color surfaceContainer = Color(0xFFFFFFFF); // Container surface
|
||||
static const Color surfaceContainerHigh = Color(0xFFF8F9FA); // Container élevé
|
||||
static const Color surfaceContainerHighest = Color(0xFFE5E7EB); // Container max
|
||||
static const Color background = Color(0xFFF8F9FA); // Background général
|
||||
static const Color surface = Color(0xFFFFFFFF);
|
||||
static const Color surfaceVariant = Color(0xFFF1F8E9);
|
||||
static const Color surfaceContainer = Color(0xFFFFFFFF);
|
||||
static const Color surfaceContainerHigh = Color(0xFFF1F8E9);
|
||||
static const Color surfaceContainerHighest = Color(0xFFDCEDC8);
|
||||
static const Color background = Color(0xFFF1F8E9);
|
||||
|
||||
static const Color onSurface = Color(0xFF1F2937); // Texte principal (gris très foncé)
|
||||
static const Color onSurfaceVariant = Color(0xFF6B7280); // Texte secondaire (gris moyen)
|
||||
static const Color textSecondary = Color(0xFF6B7280); // Texte secondaire (alias)
|
||||
static const Color outline = Color(0xFFD1D5DB); // Bordures
|
||||
static const Color outlineVariant = Color(0xFFE5E7EB); // Bordures claires
|
||||
static const Color onSurface = Color(0xFF1C2B1C);
|
||||
static const Color onSurfaceVariant = Color(0xFF4E6B4E);
|
||||
static const Color textSecondary = Color(0xFF4E6B4E);
|
||||
static const Color outline = Color(0xFFC8E6C9);
|
||||
static const Color outlineVariant = Color(0xFFDCEDC8);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS NEUTRES - MODE NUIT
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static const Color surfaceDarkMode = Color(0xFF1E1E1E); // Surface principale (gris très sombre)
|
||||
static const Color surfaceVariantDarkMode = Color(0xFF2C2C2C); // Surface variante
|
||||
static const Color backgroundDarkMode = Color(0xFF121212); // Background général (noir profond)
|
||||
static const Color surfaceDarkMode = Color(0xFF1A2E1A);
|
||||
static const Color surfaceVariantDarkMode = Color(0xFF243824);
|
||||
static const Color backgroundDarkMode = Color(0xFF0F1A0F);
|
||||
|
||||
static const Color onSurfaceDarkMode = Color(0xFFE5E7EB); // Texte principal (gris très clair)
|
||||
static const Color onSurfaceVariantDarkMode = Color(0xFF9CA3AF); // Texte secondaire (gris moyen)
|
||||
static const Color outlineDarkMode = Color(0xFF4B5563); // Bordures mode nuit
|
||||
static const Color onSurfaceDarkMode = Color(0xFFE0F2E0);
|
||||
static const Color onSurfaceVariantDarkMode = Color(0xFF90B890);
|
||||
static const Color outlineDarkMode = Color(0xFF3A5E3A);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS SÉMANTIQUES - États et feedback
|
||||
// COULEURS SÉMANTIQUES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Couleurs de succès
|
||||
static const Color success = Color(0xFF10B981); // Vert succès
|
||||
static const Color successLight = Color(0xFF34D399); // Vert clair
|
||||
static const Color successDark = Color(0xFF059669); // Vert sombre
|
||||
static const Color successContainer = Color(0xFFECFDF5); // Container succès
|
||||
|
||||
static const Color success = Color(0xFF2E7D32);
|
||||
static const Color successLight = Color(0xFF4CAF50);
|
||||
static const Color successDark = Color(0xFF1B5E20);
|
||||
static const Color successContainer = Color(0xFFE8F5E9);
|
||||
static const Color onSuccess = Color(0xFFFFFFFF);
|
||||
static const Color onSuccessContainer = Color(0xFF002114);
|
||||
static const Color onSuccessContainer = Color(0xFF003908);
|
||||
|
||||
/// Couleurs d'erreur
|
||||
static const Color error = Color(0xFFDC2626); // Rouge erreur
|
||||
static const Color errorLight = Color(0xFFEF4444); // Rouge clair
|
||||
static const Color errorDark = Color(0xFFB91C1C); // Rouge sombre
|
||||
static const Color errorContainer = Color(0xFFFEF2F2); // Container erreur
|
||||
static const Color error = Color(0xFFDC2626);
|
||||
static const Color errorLight = Color(0xFFEF4444);
|
||||
static const Color errorDark = Color(0xFFB91C1C);
|
||||
static const Color errorContainer = Color(0xFFFEF2F2);
|
||||
static const Color onError = Color(0xFFFFFFFF);
|
||||
static const Color onErrorContainer = Color(0xFF410002);
|
||||
|
||||
/// Couleurs d'avertissement
|
||||
static const Color warning = Color(0xFFF59E0B); // Orange avertissement
|
||||
static const Color warningLight = Color(0xFFFBBF24); // Orange clair
|
||||
static const Color warningDark = Color(0xFFD97706); // Orange sombre
|
||||
static const Color warningContainer = Color(0xFFFEF3C7); // Container avertissement
|
||||
static const Color warning = Color(0xFFF59E0B);
|
||||
static const Color warningLight = Color(0xFFFBBF24);
|
||||
static const Color warningDark = Color(0xFFD97706);
|
||||
static const Color warningContainer = Color(0xFFFEF3C7);
|
||||
static const Color onWarning = Color(0xFFFFFFFF);
|
||||
static const Color onWarningContainer = Color(0xFF2D1B00);
|
||||
|
||||
/// Couleurs d'information
|
||||
static const Color info = Color(0xFF0EA5E9); // Bleu info
|
||||
static const Color infoLight = Color(0xFF38BDF8); // Bleu clair
|
||||
static const Color infoDark = Color(0xFF0284C7); // Bleu sombre
|
||||
static const Color infoContainer = Color(0xFFE0F2FE); // Container info
|
||||
static const Color info = Color(0xFF0288D1);
|
||||
static const Color infoLight = Color(0xFF29B6F6);
|
||||
static const Color infoDark = Color(0xFF01579B);
|
||||
static const Color infoContainer = Color(0xFFE1F5FE);
|
||||
static const Color onInfo = Color(0xFFFFFFFF);
|
||||
static const Color onInfoContainer = Color(0xFF001D36);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COULEURS SPÉCIALISÉES - Interface avancée
|
||||
// COULEURS DE NAVIGATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Couleurs de navigation - Mode Jour
|
||||
|
||||
static const Color navigationBackground = Color(0xFFFFFFFF);
|
||||
static const Color navigationSelected = Color(0xFF4169E1); // Bleu roi
|
||||
static const Color navigationUnselected = Color(0xFF6B7280);
|
||||
static const Color navigationIndicator = Color(0xFF4169E1); // Bleu roi
|
||||
static const Color navigationSelected = Color(0xFF2E7D32);
|
||||
static const Color navigationUnselected = Color(0xFF4E6B4E);
|
||||
static const Color navigationIndicator = Color(0xFFE8F5E9);
|
||||
|
||||
/// Couleurs de navigation - Mode Nuit
|
||||
static const Color navigationBackgroundDarkMode = Color(0xFF1E1E1E);
|
||||
static const Color navigationSelectedDarkMode = Color(0xFF2C5F6F); // Bleu pétrole
|
||||
static const Color navigationUnselectedDarkMode = Color(0xFF9CA3AF);
|
||||
static const Color navigationIndicatorDarkMode = Color(0xFF2C5F6F); // Bleu pétrole
|
||||
static const Color navigationBackgroundDarkMode = Color(0xFF1A2E1A);
|
||||
static const Color navigationSelectedDarkMode = Color(0xFFA5D6A7);
|
||||
static const Color navigationUnselectedDarkMode = Color(0xFF90B890);
|
||||
static const Color navigationIndicatorDarkMode = Color(0xFF2E4D2E);
|
||||
|
||||
/// Couleurs d'élévation et ombres
|
||||
static const Color shadow = Color(0x1A000000); // Ombre légère
|
||||
static const Color shadowMedium = Color(0x33000000); // Ombre moyenne
|
||||
static const Color shadowHigh = Color(0x4D000000); // Ombre forte
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// OMBRES ET EFFETS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Couleurs de glassmorphism (tendance 2024-2025)
|
||||
static const Color glassBackground = Color(0x80FFFFFF); // Fond verre
|
||||
static const Color glassBorder = Color(0x33FFFFFF); // Bordure verre
|
||||
static const Color glassOverlay = Color(0x0DFFFFFF); // Overlay verre
|
||||
static const Color shadow = Color(0x1A1C2B1C);
|
||||
static const Color shadowMedium = Color(0x331C2B1C);
|
||||
static const Color shadowHigh = Color(0x4D1C2B1C);
|
||||
|
||||
static const Color glassBackground = Color(0x80FFFFFF);
|
||||
static const Color glassBorder = Color(0x33FFFFFF);
|
||||
static const Color glassOverlay = Color(0x0DFFFFFF);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// GRADIENTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Couleurs de gradient - Mode Jour (Bleu Roi)
|
||||
static const List<Color> primaryGradient = [
|
||||
Color(0xFF4169E1), // Bleu roi
|
||||
Color(0xFF6B8EF5), // Bleu roi clair
|
||||
Color(0xFF1B5E20),
|
||||
Color(0xFF2E7D32),
|
||||
Color(0xFF388E3C),
|
||||
];
|
||||
|
||||
/// Couleurs de gradient - Mode Nuit (Bleu Pétrole)
|
||||
static const List<Color> primaryGradientDarkMode = [
|
||||
Color(0xFF2C5F6F), // Bleu pétrole
|
||||
Color(0xFF3D7A8C), // Bleu pétrole clair
|
||||
Color(0xFF0F1A0F),
|
||||
Color(0xFF1A2E1A),
|
||||
Color(0xFF243824),
|
||||
];
|
||||
|
||||
static const List<Color> secondaryGradient = [
|
||||
Color(0xFF6366F1), // Indigo
|
||||
Color(0xFF8B8FF6), // Indigo clair
|
||||
Color(0xFF388E3C),
|
||||
Color(0xFF66BB6A),
|
||||
];
|
||||
|
||||
static const List<Color> successGradient = [
|
||||
Color(0xFF10B981), // Vert émeraude
|
||||
Color(0xFF34D399), // Vert clair
|
||||
Color(0xFF2E7D32),
|
||||
Color(0xFF4CAF50),
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// MÉTHODES UTILITAIRES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Obtient une couleur avec opacité
|
||||
|
||||
static Color withOpacity(Color color, double opacity) {
|
||||
return color.withOpacity(opacity);
|
||||
}
|
||||
|
||||
/// Obtient une couleur plus claire
|
||||
|
||||
static Color lighten(Color color, [double amount = 0.1]) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final lightness = (hsl.lightness + amount).clamp(0.0, 1.0);
|
||||
return hsl.withLightness(lightness).toColor();
|
||||
}
|
||||
|
||||
/// Obtient une couleur plus sombre
|
||||
|
||||
static Color darken(Color color, [double amount = 0.1]) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final lightness = (hsl.lightness - amount).clamp(0.0, 1.0);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
library typography_tokens;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'color_tokens.dart';
|
||||
|
||||
/// Tokens typographiques - Système de texte moderne
|
||||
@@ -15,12 +16,12 @@ class TypographyTokens {
|
||||
// FAMILLES DE POLICES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Police principale - Inter (moderne et lisible)
|
||||
static const String primaryFontFamily = 'Inter';
|
||||
|
||||
/// Police secondaire - SF Pro Display (élégante)
|
||||
static const String secondaryFontFamily = 'SF Pro Display';
|
||||
|
||||
/// Police principale - Roboto (Google Fonts, cross-platform)
|
||||
static const String primaryFontFamily = 'Roboto';
|
||||
|
||||
/// Police display - Roboto (Google Fonts, titres)
|
||||
static const String displayFontFamily = 'Roboto';
|
||||
|
||||
/// Police monospace - JetBrains Mono (code et données)
|
||||
static const String monospaceFontFamily = 'JetBrains Mono';
|
||||
|
||||
@@ -28,33 +29,30 @@ class TypographyTokens {
|
||||
// ÉCHELLE TYPOGRAPHIQUE - Basée sur Material Design 3
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Display - Titres principaux et héros
|
||||
static const TextStyle displayLarge = TextStyle(
|
||||
fontFamily: primaryFontFamily,
|
||||
fontSize: 57.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.25,
|
||||
height: 1.12,
|
||||
color: ColorTokens.onSurface,
|
||||
);
|
||||
|
||||
static const TextStyle displayMedium = TextStyle(
|
||||
fontFamily: primaryFontFamily,
|
||||
fontSize: 45.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.0,
|
||||
height: 1.16,
|
||||
color: ColorTokens.onSurface,
|
||||
);
|
||||
|
||||
static const TextStyle displaySmall = TextStyle(
|
||||
fontFamily: primaryFontFamily,
|
||||
fontSize: 36.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0.0,
|
||||
height: 1.22,
|
||||
color: ColorTokens.onSurface,
|
||||
);
|
||||
/// Display — Titres principaux (Roboto via GoogleFonts)
|
||||
static TextStyle get displayLarge => GoogleFonts.roboto(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.2,
|
||||
color: ColorTokens.onSurface,
|
||||
);
|
||||
|
||||
static TextStyle get displayMedium => GoogleFonts.roboto(
|
||||
fontSize: 28.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.25,
|
||||
height: 1.25,
|
||||
color: ColorTokens.onSurface,
|
||||
);
|
||||
|
||||
static TextStyle get displaySmall => GoogleFonts.roboto(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.0,
|
||||
height: 1.3,
|
||||
color: ColorTokens.onSurface,
|
||||
);
|
||||
|
||||
/// Headline - Titres de sections
|
||||
static const TextStyle headlineLarge = TextStyle(
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
library unionflow_design_system;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// IMPORTS de base pour le Design System
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'tokens/app_colors.dart';
|
||||
import 'tokens/app_typography.dart';
|
||||
@@ -20,54 +16,83 @@ export 'theme/app_theme.dart';
|
||||
export 'components/components.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COMPATIBILITÉ - Shims pour les anciens tokens (Migration progressive)
|
||||
// SHIMS DE COMPATIBILITÉ — Migration progressive vers design system unifié
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Shim de compatibilité pour ColorTokens
|
||||
/// Shim ColorTokens — palette Vert Forêt/Ardoise
|
||||
class ColorTokens {
|
||||
static const Color primary = AppColors.primaryGreen;
|
||||
static const Color primaryContainer = AppColors.lightSurface;
|
||||
// Primaires
|
||||
static const Color primary = AppColors.primaryGreen; // #2E7D32
|
||||
static const Color primaryLight = AppColors.brandGreenLight; // #4CAF50
|
||||
static const Color primaryDark = AppColors.brandGreen; // #1B5E20
|
||||
static const Color primaryContainer = Color(0xFFE8F5E9);
|
||||
static const Color onPrimary = Colors.white;
|
||||
static const Color onPrimaryContainer = AppColors.textPrimaryLight;
|
||||
static const Color secondary = AppColors.brandGreen;
|
||||
static const Color secondaryContainer = AppColors.lightSurface;
|
||||
|
||||
// Secondaires
|
||||
static const Color secondary = AppColors.brandGreenLight;
|
||||
static const Color secondaryContainer = Color(0xFFC8E6C9);
|
||||
static const Color onSecondary = Colors.white;
|
||||
static const Color tertiary = AppColors.brandGreenLight;
|
||||
static const Color tertiaryContainer = AppColors.lightSurface;
|
||||
static const Color onSecondaryContainer = AppColors.textPrimaryLight;
|
||||
|
||||
// Tertiaires
|
||||
static const Color tertiary = AppColors.brandMint;
|
||||
static const Color tertiaryContainer = Color(0xFFDCEDC8);
|
||||
static const Color onTertiary = Colors.white;
|
||||
|
||||
// Surfaces
|
||||
static const Color surface = AppColors.lightSurface;
|
||||
static const Color surfaceVariant = AppColors.lightSurface;
|
||||
static const Color surfaceVariant = AppColors.lightBackground;
|
||||
static const Color surfaceContainer = AppColors.lightSurface;
|
||||
static const Color background = AppColors.lightBackground;
|
||||
static const Color onSurface = AppColors.textPrimaryLight;
|
||||
static const Color onSurfaceVariant = AppColors.textSecondaryLight;
|
||||
static const Color outline = AppColors.lightBorder;
|
||||
static const Color outlineVariant = AppColors.lightBorder;
|
||||
static const Color outlineVariant = Color(0xFFDCEDC8);
|
||||
|
||||
// Erreur / succès
|
||||
static const Color error = AppColors.error;
|
||||
static const Color onError = Colors.white;
|
||||
static const Color errorContainer = Color(0xFFFEF2F2);
|
||||
static const Color success = AppColors.success;
|
||||
static const Color onSuccess = Colors.white;
|
||||
static const Color info = Color(0xFF2196F3);
|
||||
static const Color warning = Color(0xFFFFC107);
|
||||
static const Color shadow = Color(0x1A000000);
|
||||
|
||||
static const Color warning = AppColors.warning;
|
||||
static const Color info = AppColors.info;
|
||||
|
||||
// Navigation
|
||||
static const Color navigationBackground = AppColors.lightSurface;
|
||||
static const Color navigationSelected = AppColors.primaryGreen;
|
||||
static const Color navigationUnselected = AppColors.textSecondaryLight;
|
||||
static const Color navigationIndicator = Color(0xFFE8F5E9);
|
||||
|
||||
// Ombres
|
||||
static const Color shadow = Color(0x1A1C2B1C);
|
||||
static const Color shadowMedium = Color(0x331C2B1C);
|
||||
|
||||
// Verre / glassmorphism
|
||||
static const Color glassBackground = Color(0x80FFFFFF);
|
||||
static const Color glassBorder = Color(0x33FFFFFF);
|
||||
|
||||
// Gradients
|
||||
static const List<Color> primaryGradient = [
|
||||
AppColors.brandGreen,
|
||||
AppColors.primaryGreen,
|
||||
AppColors.brandGreenLight,
|
||||
Color(0xFF388E3C),
|
||||
];
|
||||
}
|
||||
|
||||
/// Shim de compatibilité pour ShadowTokens
|
||||
/// Shim ShadowTokens
|
||||
class ShadowTokens {
|
||||
static const List<BoxShadow> sm = [
|
||||
BoxShadow(
|
||||
color: Color(0x1A000000),
|
||||
color: Color(0x1A1C2B1C),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
];
|
||||
static const List<BoxShadow> md = [
|
||||
BoxShadow(
|
||||
color: Color(0x26000000),
|
||||
color: Color(0x261C2B1C),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
@@ -75,33 +100,58 @@ class ShadowTokens {
|
||||
static const List<BoxShadow> primary = md;
|
||||
}
|
||||
|
||||
/// Shim de compatibilité pour RadiusTokens
|
||||
/// Shim RadiusTokens
|
||||
class RadiusTokens {
|
||||
static const double sm = SpacingTokens.radiusSm;
|
||||
static const double md = SpacingTokens.radiusMd;
|
||||
static const double lg = SpacingTokens.radiusLg;
|
||||
static const double xl = SpacingTokens.radiusXl;
|
||||
static const double circular = SpacingTokens.radiusCircular;
|
||||
static const double round = SpacingTokens.radiusCircular; // Ajouté pour compatibilité
|
||||
static const double round = SpacingTokens.radiusCircular;
|
||||
}
|
||||
|
||||
/// Shim de compatibilité pour TypographyTokens
|
||||
/// Shim TypographyTokens
|
||||
class TypographyTokens {
|
||||
static const TextStyle displayLarge = AppTypography.headerSmall;
|
||||
static const TextStyle displayMedium = AppTypography.headerSmall;
|
||||
static const TextStyle displaySmall = AppTypography.headerSmall;
|
||||
static const TextStyle headlineLarge = AppTypography.headerSmall;
|
||||
// Display (Playfair Display via AppTypography getters — non-const)
|
||||
static TextStyle get displayLarge => AppTypography.displayLarge;
|
||||
static TextStyle get displayMedium => AppTypography.displayMedium;
|
||||
static TextStyle get displaySmall => AppTypography.displaySmall;
|
||||
|
||||
// Headlines
|
||||
static const TextStyle headlineLarge = AppTypography.headerLarge;
|
||||
static const TextStyle headlineMedium = AppTypography.headerSmall;
|
||||
static const TextStyle headlineSmall = AppTypography.headerSmall;
|
||||
static const TextStyle headlineSmall = AppTypography.titleMedium;
|
||||
|
||||
// Titles
|
||||
static const TextStyle titleLarge = AppTypography.headerSmall;
|
||||
static const TextStyle titleMedium = AppTypography.headerSmall;
|
||||
static const TextStyle titleSmall = AppTypography.headerSmall;
|
||||
static const TextStyle bodyLarge = AppTypography.bodyTextSmall;
|
||||
static const TextStyle bodyMedium = AppTypography.bodyTextSmall;
|
||||
static const TextStyle bodySmall = AppTypography.subtitleSmall;
|
||||
static const TextStyle titleMedium = AppTypography.titleMedium;
|
||||
static const TextStyle titleSmall = AppTypography.titleSmall;
|
||||
|
||||
// Body
|
||||
static const TextStyle bodyLarge = AppTypography.bodyLarge;
|
||||
static const TextStyle bodyMedium = AppTypography.bodyMedium;
|
||||
static const TextStyle bodySmall = AppTypography.bodyTextSmall;
|
||||
|
||||
// Labels
|
||||
static const TextStyle labelLarge = AppTypography.actionText;
|
||||
static const TextStyle labelMedium = AppTypography.badgeText;
|
||||
static const TextStyle labelMedium = AppTypography.labelMedium;
|
||||
static const TextStyle labelSmall = AppTypography.badgeText;
|
||||
static const TextStyle buttonLarge = AppTypography.actionText;
|
||||
static const TextStyle cardValue = AppTypography.headerSmall;
|
||||
|
||||
// Buttons
|
||||
static const TextStyle buttonLarge = AppTypography.buttonLabel;
|
||||
static const TextStyle buttonMedium = AppTypography.actionText;
|
||||
|
||||
// Cards
|
||||
static const TextStyle cardTitle = AppTypography.headerSmall;
|
||||
static const TextStyle cardSubtitle = AppTypography.bodyTextSmall;
|
||||
static const TextStyle cardValue = AppTypography.headerLarge;
|
||||
|
||||
// Inputs
|
||||
static const TextStyle inputLabel = AppTypography.labelMedium;
|
||||
static const TextStyle inputText = AppTypography.bodyLarge;
|
||||
static const TextStyle inputHint = AppTypography.bodyTextSmall;
|
||||
|
||||
// Navigation
|
||||
static const TextStyle navigationLabel = AppTypography.navLabel;
|
||||
static const TextStyle navigationLabelSelected = AppTypography.navLabelSelected;
|
||||
}
|
||||
|
||||
@@ -25,4 +25,8 @@ export 'components/union_unified_account_card.dart';
|
||||
export 'components/union_period_filter.dart';
|
||||
export 'components/union_export_button.dart';
|
||||
export 'components/union_notification_badge.dart';
|
||||
export 'components/user_identity_card.dart';
|
||||
export 'components/uf_section_header.dart';
|
||||
export 'components/dashboard_activity_row.dart';
|
||||
export 'components/dashboard_event_row.dart';
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ class CoreCard extends StatelessWidget {
|
||||
const CoreCard({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.all(12.0),
|
||||
this.margin = const EdgeInsets.only(bottom: 10.0),
|
||||
this.padding = const EdgeInsets.all(8.0),
|
||||
this.margin = const EdgeInsets.only(bottom: 6.0),
|
||||
this.onTap,
|
||||
this.backgroundColor,
|
||||
}) : super(key: key);
|
||||
@@ -23,22 +23,22 @@ class CoreCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? (isDark ? const Color(0xFF1A1A1A) : Colors.white),
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
color: backgroundColor ?? (isDark ? AppColors.darkSurface : Colors.white),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
border: Border.all(
|
||||
color: isDark ? AppColors.darkBorder.withOpacity(0.5) : AppColors.lightBorder,
|
||||
width: 0.4,
|
||||
width: 0.8,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(isDark ? 0.3 : 0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
color: Colors.black.withOpacity(isDark ? 0.15 : 0.04),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -46,7 +46,7 @@ class CoreCard extends StatelessWidget {
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
|
||||
@@ -44,7 +44,7 @@ class CoreTextField extends StatelessWidget {
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
@@ -63,7 +63,7 @@ class CoreTextField extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryGreen,
|
||||
width: 1.5,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorText: errorText,
|
||||
|
||||
@@ -21,17 +21,17 @@ class ErrorDisplayWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Error icon
|
||||
Icon(
|
||||
_getErrorIcon(),
|
||||
size: 64,
|
||||
size: 40,
|
||||
color: _getErrorColor(context),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Error title
|
||||
Text(
|
||||
@@ -55,7 +55,7 @@ class ErrorDisplayWidget extends StatelessWidget {
|
||||
|
||||
// Retry button (if retryable and callback provided)
|
||||
if (showRetryButton && failure.isRetryable && onRetry != null) ...[
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
@@ -63,7 +63,7 @@ class ErrorDisplayWidget extends StatelessWidget {
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -151,7 +151,7 @@ class ErrorBanner extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: _getErrorColor(context).withOpacity(0.1),
|
||||
border: Border.all(
|
||||
|
||||
@@ -29,17 +29,17 @@ class AppErrorWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.error_outline,
|
||||
size: 64,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title ?? 'Oups !',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
@@ -56,7 +56,7 @@ class AppErrorWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
@@ -64,7 +64,7 @@ class AppErrorWidget extends StatelessWidget {
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -134,17 +134,17 @@ class EmptyDataWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.inbox_outlined,
|
||||
size: 64,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
@@ -153,7 +153,7 @@ class EmptyDataWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (onAction != null && actionLabel != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: onAction,
|
||||
child: Text(actionLabel!),
|
||||
|
||||
@@ -33,7 +33,7 @@ class AppLoadingWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
@@ -62,7 +62,7 @@ class ShimmerListLoading extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: itemCount,
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(10),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
@@ -127,7 +127,7 @@ class ShimmerGridLoading extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(10),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: 12,
|
||||
@@ -161,7 +161,7 @@ class ShimmerDetailLoading extends StatelessWidget {
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -173,7 +173,7 @@ class ShimmerDetailLoading extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
// Title
|
||||
Container(
|
||||
height: 24,
|
||||
@@ -187,7 +187,7 @@ class ShimmerDetailLoading extends StatelessWidget {
|
||||
width: 200,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 12),
|
||||
// Content lines
|
||||
...List.generate(5, (index) {
|
||||
return Padding(
|
||||
|
||||
Reference in New Issue
Block a user