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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user