Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Background avec motifs géométriques africains subtils
|
||||
class AfricanPatternBackground extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Color? patternColor;
|
||||
final double opacity;
|
||||
|
||||
const AfricanPatternBackground({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.patternColor,
|
||||
this.opacity = 0.03,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Background avec motifs
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: CustomPaint(
|
||||
painter: AfricanPatternPainter(
|
||||
color: (patternColor ?? UnionFlowColors.unionGreen).withOpacity(opacity),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Contenu
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Painter pour dessiner les motifs africains
|
||||
class AfricanPatternPainter extends CustomPainter {
|
||||
final Color color;
|
||||
|
||||
AfricanPatternPainter({required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
final fillPaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Espacement entre les motifs
|
||||
const double spacing = 80.0;
|
||||
const double patternSize = 40.0;
|
||||
|
||||
// Dessiner la grille de motifs
|
||||
for (double y = 0; y < size.height + spacing; y += spacing) {
|
||||
for (double x = 0; x < size.width + spacing; x += spacing) {
|
||||
final offset = Offset(x, y);
|
||||
|
||||
// Alterner entre différents motifs
|
||||
final patternType = ((x ~/ spacing) + (y ~/ spacing)) % 3;
|
||||
|
||||
switch (patternType) {
|
||||
case 0:
|
||||
_drawDiamondPattern(canvas, offset, patternSize, paint);
|
||||
break;
|
||||
case 1:
|
||||
_drawTrianglePattern(canvas, offset, patternSize, fillPaint);
|
||||
break;
|
||||
case 2:
|
||||
_drawCirclePattern(canvas, offset, patternSize, paint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawDiamondPattern(Canvas canvas, Offset offset, double size, Paint paint) {
|
||||
final path = Path()
|
||||
..moveTo(offset.dx, offset.dy - size / 2)
|
||||
..lineTo(offset.dx + size / 2, offset.dy)
|
||||
..lineTo(offset.dx, offset.dy + size / 2)
|
||||
..lineTo(offset.dx - size / 2, offset.dy)
|
||||
..close();
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawTrianglePattern(Canvas canvas, Offset offset, double size, Paint paint) {
|
||||
final path = Path()
|
||||
..moveTo(offset.dx, offset.dy - size / 3)
|
||||
..lineTo(offset.dx + size / 3, offset.dy + size / 3)
|
||||
..lineTo(offset.dx - size / 3, offset.dy + size / 3)
|
||||
..close();
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawCirclePattern(Canvas canvas, Offset offset, double size, Paint paint) {
|
||||
canvas.drawCircle(offset, size / 4, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
60
lib/shared/design_system/components/animated_fade_in.dart
Normal file
60
lib/shared/design_system/components/animated_fade_in.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Widget avec animation de fade-in automatique
|
||||
class AnimatedFadeIn extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration duration;
|
||||
final Duration delay;
|
||||
final Curve curve;
|
||||
|
||||
const AnimatedFadeIn({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.duration = const Duration(milliseconds: 600),
|
||||
this.delay = Duration.zero,
|
||||
this.curve = Curves.easeOut,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedFadeIn> createState() => _AnimatedFadeInState();
|
||||
}
|
||||
|
||||
class _AnimatedFadeInState extends State<AnimatedFadeIn>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: widget.curve,
|
||||
);
|
||||
|
||||
Future.delayed(widget.delay, () {
|
||||
if (mounted) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
74
lib/shared/design_system/components/animated_slide_in.dart
Normal file
74
lib/shared/design_system/components/animated_slide_in.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Widget avec animation de slide-in automatique
|
||||
class AnimatedSlideIn extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration duration;
|
||||
final Duration delay;
|
||||
final Offset begin;
|
||||
final Curve curve;
|
||||
|
||||
const AnimatedSlideIn({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.duration = const Duration(milliseconds: 600),
|
||||
this.delay = Duration.zero,
|
||||
this.begin = const Offset(0, 0.3),
|
||||
this.curve = Curves.easeOut,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedSlideIn> createState() => _AnimatedSlideInState();
|
||||
}
|
||||
|
||||
class _AnimatedSlideInState extends State<AnimatedSlideIn>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: widget.begin,
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: widget.curve,
|
||||
));
|
||||
|
||||
_fadeAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: widget.curve,
|
||||
);
|
||||
|
||||
Future.delayed(widget.delay, () {
|
||||
if (mounted) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/// UnionFlow Primary Button - Bouton principal
|
||||
///
|
||||
/// Bouton primaire avec la couleur Bleu Roi (#4169E1)
|
||||
/// Utilisé pour les actions principales (connexion, enregistrer, valider, etc.)
|
||||
library uf_primary_button;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Bouton primaire UnionFlow
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFPrimaryButton(
|
||||
/// label: 'Connexion',
|
||||
/// onPressed: () => login(),
|
||||
/// icon: Icons.login,
|
||||
/// isLoading: false,
|
||||
/// )
|
||||
/// ```
|
||||
class UFPrimaryButton extends StatelessWidget {
|
||||
/// Texte du bouton
|
||||
final String label;
|
||||
|
||||
/// Callback appelé lors du clic
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// Indique si le bouton est en chargement
|
||||
final bool isLoading;
|
||||
|
||||
/// Icône optionnelle à gauche du texte
|
||||
final IconData? icon;
|
||||
|
||||
/// Bouton pleine largeur
|
||||
final bool isFullWidth;
|
||||
|
||||
/// Hauteur personnalisée (optionnel)
|
||||
final double? height;
|
||||
|
||||
/// Couleur de fond personnalisée (optionnel)
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Couleur du texte/icône personnalisée (optionnel)
|
||||
final Color? textColor;
|
||||
|
||||
const UFPrimaryButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.icon,
|
||||
this.isFullWidth = false,
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: isFullWidth ? double.infinity : null,
|
||||
height: height ?? SpacingTokens.buttonHeightLarge,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
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,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
],
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/// UnionFlow Secondary Button - Bouton secondaire
|
||||
///
|
||||
/// Bouton secondaire avec la couleur Indigo (#6366F1)
|
||||
/// Utilisé pour les actions secondaires (annuler, retour, etc.)
|
||||
library uf_secondary_button;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Bouton secondaire UnionFlow
|
||||
class UFSecondaryButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final bool isLoading;
|
||||
final IconData? icon;
|
||||
final bool isFullWidth;
|
||||
final double? height;
|
||||
|
||||
const UFSecondaryButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.icon,
|
||||
this.isFullWidth = false,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: isFullWidth ? double.infinity : null,
|
||||
height: height ?? SpacingTokens.buttonHeightLarge,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
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,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
],
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
158
lib/shared/design_system/components/cards/uf_card.dart
Normal file
158
lib/shared/design_system/components/cards/uf_card.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Card standardisé UnionFlow
|
||||
///
|
||||
/// Composant Card unifié avec 3 styles prédéfinis :
|
||||
/// - elevated : Card avec ombre (par défaut)
|
||||
/// - outlined : Card avec bordure
|
||||
/// - filled : Card avec fond coloré
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFCard(
|
||||
/// child: Text('Contenu'),
|
||||
/// )
|
||||
///
|
||||
/// UFCard.outlined(
|
||||
/// child: Text('Contenu'),
|
||||
/// )
|
||||
///
|
||||
/// UFCard.filled(
|
||||
/// color: ColorTokens.primary,
|
||||
/// child: Text('Contenu'),
|
||||
/// )
|
||||
/// ```
|
||||
class UFCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsets? padding;
|
||||
final EdgeInsets? margin;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final UFCardStyle style;
|
||||
final Color? color;
|
||||
final Color? borderColor;
|
||||
final double? borderWidth;
|
||||
final double? elevation;
|
||||
final double? borderRadius;
|
||||
|
||||
/// Card avec ombre (style par défaut)
|
||||
const UFCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.color,
|
||||
this.elevation,
|
||||
this.borderRadius,
|
||||
}) : style = UFCardStyle.elevated,
|
||||
borderColor = null,
|
||||
borderWidth = null;
|
||||
|
||||
/// Card avec bordure
|
||||
const UFCard.outlined({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.color,
|
||||
this.borderColor,
|
||||
this.borderWidth,
|
||||
this.borderRadius,
|
||||
}) : style = UFCardStyle.outlined,
|
||||
elevation = null;
|
||||
|
||||
/// Card avec fond coloré
|
||||
const UFCard.filled({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
required this.color,
|
||||
this.borderRadius,
|
||||
}) : style = UFCardStyle.filled,
|
||||
borderColor = null,
|
||||
borderWidth = null,
|
||||
elevation = null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.cardPadding);
|
||||
final effectiveMargin = margin ?? EdgeInsets.zero;
|
||||
final effectiveBorderRadius = borderRadius ?? SpacingTokens.radiusLg;
|
||||
|
||||
Widget content = Container(
|
||||
padding: effectivePadding,
|
||||
decoration: _getDecoration(effectiveBorderRadius),
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (onTap != null || onLongPress != null) {
|
||||
content = InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
borderRadius: BorderRadius.circular(effectiveBorderRadius),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: effectiveMargin,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _getDecoration(double radius) {
|
||||
switch (style) {
|
||||
case UFCardStyle.elevated:
|
||||
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:
|
||||
return BoxDecoration(
|
||||
color: color ?? AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
border: Border.all(
|
||||
color: borderColor ?? AppColors.lightBorder,
|
||||
width: borderWidth ?? 1.0,
|
||||
),
|
||||
);
|
||||
|
||||
case UFCardStyle.filled:
|
||||
return BoxDecoration(
|
||||
color: color ?? AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Styles de Card disponibles
|
||||
enum UFCardStyle {
|
||||
/// Card avec ombre
|
||||
elevated,
|
||||
|
||||
/// Card avec bordure
|
||||
outlined,
|
||||
|
||||
/// Card avec fond coloré
|
||||
filled,
|
||||
}
|
||||
|
||||
89
lib/shared/design_system/components/cards/uf_info_card.dart
Normal file
89
lib/shared/design_system/components/cards/uf_info_card.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
/// UnionFlow Info Card - Card d'information générique
|
||||
///
|
||||
/// Card blanche avec titre, icône et contenu personnalisable
|
||||
library uf_info_card;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Card d'information générique
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFInfoCard(
|
||||
/// title: 'État du système',
|
||||
/// icon: Icons.health_and_safety,
|
||||
/// iconColor: ColorTokens.primary,
|
||||
/// trailing: Container(...), // Badge ou autre widget
|
||||
/// child: Column(...), // Contenu de la card
|
||||
/// )
|
||||
/// ```
|
||||
class UFInfoCard extends StatelessWidget {
|
||||
/// Titre de la card
|
||||
final String title;
|
||||
|
||||
/// Icône du titre
|
||||
final IconData icon;
|
||||
|
||||
/// Couleur de l'icône (par défaut: primary)
|
||||
final Color? iconColor;
|
||||
|
||||
/// Widget à droite du titre (badge, bouton, etc.)
|
||||
final Widget? trailing;
|
||||
|
||||
/// Contenu de la card
|
||||
final Widget child;
|
||||
|
||||
/// Padding de la card (par défaut: xl)
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const UFInfoCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.iconColor,
|
||||
this.trailing,
|
||||
required this.child,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.xl);
|
||||
|
||||
return Container(
|
||||
padding: effectivePadding,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
boxShadow: ShadowTokens.sm,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header avec titre et trailing
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: effectiveIconColor, size: 20),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
// Contenu
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/// UnionFlow Metric Card - Card de métrique système
|
||||
///
|
||||
/// Card compacte pour afficher une métrique système (CPU, RAM, etc.)
|
||||
library uf_metric_card;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Card de métrique système
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFMetricCard(
|
||||
/// label: 'CPU',
|
||||
/// value: '23.5%',
|
||||
/// icon: Icons.memory,
|
||||
/// color: ColorTokens.success,
|
||||
/// )
|
||||
/// ```
|
||||
class UFMetricCard extends StatelessWidget {
|
||||
/// Label de la métrique (ex: "CPU")
|
||||
final String label;
|
||||
|
||||
/// Valeur de la métrique (ex: "23.5%")
|
||||
final String value;
|
||||
|
||||
/// Icône représentant la métrique
|
||||
final IconData icon;
|
||||
|
||||
/// Couleur de la métrique (optionnel)
|
||||
final Color? color;
|
||||
|
||||
const UFMetricCard({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 16),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
fontSize: 9,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
141
lib/shared/design_system/components/cards/uf_stat_card.dart
Normal file
141
lib/shared/design_system/components/cards/uf_stat_card.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
/// UnionFlow Stat Card - Card de statistiques
|
||||
///
|
||||
/// Card affichant une statistique avec icône, titre, valeur et sous-titre optionnel
|
||||
/// Utilisé dans le dashboard pour afficher les métriques clés
|
||||
library uf_stat_card;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Card de statistiques UnionFlow
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFStatCard(
|
||||
/// title: 'Membres',
|
||||
/// value: '142',
|
||||
/// icon: Icons.people,
|
||||
/// iconColor: ColorTokens.primary,
|
||||
/// subtitle: '+5 ce mois',
|
||||
/// onTap: () => navigateToMembers(),
|
||||
/// )
|
||||
/// ```
|
||||
class UFStatCard extends StatelessWidget {
|
||||
/// Titre de la statistique (ex: "Membres")
|
||||
final String title;
|
||||
|
||||
/// Valeur de la statistique (ex: "142")
|
||||
final String value;
|
||||
|
||||
/// Icône représentant la statistique
|
||||
final IconData icon;
|
||||
|
||||
/// Couleur de l'icône (par défaut: primary)
|
||||
final Color? iconColor;
|
||||
|
||||
/// Sous-titre optionnel (ex: "+5 ce mois")
|
||||
final String? subtitle;
|
||||
|
||||
/// Callback appelé lors du clic sur la card
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Couleur de fond de l'icône (par défaut: iconColor avec opacité)
|
||||
final Color? iconBackgroundColor;
|
||||
|
||||
const UFStatCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
this.iconColor,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
this.iconBackgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
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),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(SpacingTokens.cardPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header avec icône et flèche
|
||||
Row(
|
||||
children: [
|
||||
// Icône avec background coloré
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveIconBgColor,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: effectiveIconColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Flèche si cliquable
|
||||
if (onTap != null)
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
|
||||
// Titre
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
|
||||
// Valeur
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
|
||||
// Sous-titre optionnel
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
43
lib/shared/design_system/components/components.dart
Normal file
43
lib/shared/design_system/components/components.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
/// UnionFlow Components - Export centralisé
|
||||
///
|
||||
/// Ce fichier exporte tous les composants réutilisables du Design System
|
||||
library components;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BOUTONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export 'buttons/uf_primary_button.dart';
|
||||
export 'buttons/uf_secondary_button.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CARDS & CONTAINERS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export 'cards/uf_card.dart';
|
||||
export 'cards/uf_stat_card.dart';
|
||||
export 'cards/uf_info_card.dart';
|
||||
export 'cards/uf_metric_card.dart';
|
||||
export 'uf_container.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// INPUTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export 'inputs/uf_switch_tile.dart';
|
||||
export 'inputs/uf_dropdown_tile.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// HEADERS & APPBAR
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export 'uf_header.dart';
|
||||
export 'uf_page_header.dart';
|
||||
export 'uf_app_bar.dart';
|
||||
|
||||
// Composants supplémentaires à exporter quand créés :
|
||||
// export 'buttons/uf_outline_button.dart';
|
||||
// export 'buttons/uf_text_button.dart';
|
||||
// export 'cards/uf_event_card.dart';
|
||||
// export 'inputs/uf_text_field.dart';
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/// UnionFlow Dropdown Tile - Ligne de paramètre avec dropdown
|
||||
///
|
||||
/// Tile avec titre et dropdown pour les paramètres
|
||||
library uf_dropdown_tile;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Tile de paramètre avec dropdown
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFDropdownTile<String>(
|
||||
/// title: 'Niveau de log',
|
||||
/// value: 'INFO',
|
||||
/// items: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'],
|
||||
/// onChanged: (value) => setState(() => _logLevel = value),
|
||||
/// )
|
||||
/// ```
|
||||
class UFDropdownTile<T> extends StatelessWidget {
|
||||
/// Titre du paramètre
|
||||
final String title;
|
||||
|
||||
/// Valeur actuelle
|
||||
final T value;
|
||||
|
||||
/// Liste des options
|
||||
final List<T> items;
|
||||
|
||||
/// Callback appelé lors du changement
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// Couleur de fond (par défaut: surfaceVariant)
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Fonction pour afficher le texte d'un item (par défaut: toString())
|
||||
final String Function(T)? itemBuilder;
|
||||
|
||||
const UFDropdownTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.items,
|
||||
this.onChanged,
|
||||
this.backgroundColor,
|
||||
this.itemBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
|
||||
final effectiveItemBuilder = itemBuilder ?? (item) => item.toString();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveBgColor,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
border: Border.all(color: AppColors.lightBorder),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<T>(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
items: items.map((item) {
|
||||
return DropdownMenuItem<T>(
|
||||
value: item,
|
||||
child: Text(effectiveItemBuilder(item)),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/// UnionFlow Switch Tile - Ligne de paramètre avec switch
|
||||
///
|
||||
/// Tile avec titre, description et switch pour les paramètres
|
||||
library uf_switch_tile;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../unionflow_design_system.dart';
|
||||
|
||||
/// Tile de paramètre avec switch
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFSwitchTile(
|
||||
/// title: 'Notifications',
|
||||
/// subtitle: 'Activer les notifications push',
|
||||
/// value: true,
|
||||
/// onChanged: (value) => setState(() => _notifications = value),
|
||||
/// )
|
||||
/// ```
|
||||
class UFSwitchTile extends StatelessWidget {
|
||||
/// Titre du paramètre
|
||||
final String title;
|
||||
|
||||
/// Description du paramètre
|
||||
final String subtitle;
|
||||
|
||||
/// Valeur actuelle du switch
|
||||
final bool value;
|
||||
|
||||
/// Callback appelé lors du changement
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
/// Couleur de fond (par défaut: surfaceVariant)
|
||||
final Color? backgroundColor;
|
||||
|
||||
const UFSwitchTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveBgColor = backgroundColor ?? AppColors.lightSurface;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
|
||||
padding: const EdgeInsets.all(SpacingTokens.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveBgColor,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
103
lib/shared/design_system/components/uf_app_bar.dart
Normal file
103
lib/shared/design_system/components/uf_app_bar.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../unionflow_design_system.dart';
|
||||
|
||||
/// AppBar standardisé UnionFlow
|
||||
///
|
||||
/// Composant AppBar unifié pour toutes les pages de détail/formulaire.
|
||||
/// Garantit la cohérence visuelle et l'expérience utilisateur.
|
||||
///
|
||||
/// Si [mergeLeadingWithTitle] est true et que la route peut être quittée,
|
||||
/// le bouton retour et le titre sont fusionnés en une seule ligne (retour
|
||||
/// toujours visible avec la même couleur que le titre).
|
||||
class UFAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final List<Widget>? actions;
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
/// Fusionne le bouton retour et le titre en une seule zone (retour visible, même couleur que le titre).
|
||||
final bool mergeLeadingWithTitle;
|
||||
final PreferredSizeWidget? bottom;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final double elevation;
|
||||
|
||||
const UFAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.leading,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.mergeLeadingWithTitle = false,
|
||||
this.bottom,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.elevation = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPop = ModalRoute.of(context)?.canPop ?? false;
|
||||
final fg = foregroundColor ?? Colors.white;
|
||||
final useMergedTitle = mergeLeadingWithTitle && canPop;
|
||||
|
||||
final isTransparent = backgroundColor == Colors.transparent ||
|
||||
(backgroundColor != null && backgroundColor!.opacity < 0.1);
|
||||
|
||||
return AppBar(
|
||||
title: useMergedTitle
|
||||
? Row(
|
||||
children: [
|
||||
Material(
|
||||
color: isTransparent ? Colors.black26 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
color: fg,
|
||||
tooltip: 'Retour',
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: fg,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(title),
|
||||
backgroundColor: backgroundColor ?? AppColors.primaryGreen,
|
||||
foregroundColor: fg,
|
||||
elevation: elevation,
|
||||
leading: useMergedTitle ? null : leading,
|
||||
automaticallyImplyLeading: useMergedTitle ? false : automaticallyImplyLeading,
|
||||
actions: actions,
|
||||
bottom: bottom,
|
||||
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
),
|
||||
centerTitle: false,
|
||||
titleTextStyle: AppTypography.headerSmall.copyWith(
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(
|
||||
kToolbarHeight + (bottom?.preferredSize.height ?? 0.0),
|
||||
);
|
||||
}
|
||||
|
||||
2
lib/shared/design_system/components/uf_buttons.dart
Normal file
2
lib/shared/design_system/components/uf_buttons.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'buttons/uf_primary_button.dart';
|
||||
export 'buttons/uf_secondary_button.dart';
|
||||
138
lib/shared/design_system/components/uf_container.dart
Normal file
138
lib/shared/design_system/components/uf_container.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../unionflow_design_system.dart';
|
||||
|
||||
/// Container standardisé UnionFlow
|
||||
///
|
||||
/// Composant Container unifié avec styles prédéfinis.
|
||||
/// Garantit la cohérence des espacements, rayons et ombres.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFContainer(
|
||||
/// child: Text('Contenu'),
|
||||
/// )
|
||||
///
|
||||
/// UFContainer.rounded(
|
||||
/// color: ColorTokens.primary,
|
||||
/// child: Text('Contenu'),
|
||||
/// )
|
||||
///
|
||||
/// UFContainer.elevated(
|
||||
/// child: Text('Contenu'),
|
||||
/// )
|
||||
/// ```
|
||||
class UFContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Color? color;
|
||||
final EdgeInsets? padding;
|
||||
final EdgeInsets? margin;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final AlignmentGeometry? alignment;
|
||||
final BoxConstraints? constraints;
|
||||
final Gradient? gradient;
|
||||
final double borderRadius;
|
||||
final Border? border;
|
||||
final List<BoxShadow>? boxShadow;
|
||||
|
||||
/// Container standard
|
||||
const UFContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.alignment,
|
||||
this.constraints,
|
||||
this.gradient,
|
||||
this.border,
|
||||
this.boxShadow,
|
||||
}) : borderRadius = SpacingTokens.radiusMd;
|
||||
|
||||
/// Container avec coins arrondis
|
||||
const UFContainer.rounded({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.alignment,
|
||||
this.constraints,
|
||||
this.gradient,
|
||||
this.border,
|
||||
this.boxShadow,
|
||||
}) : borderRadius = SpacingTokens.radiusLg;
|
||||
|
||||
/// Container très arrondi
|
||||
const UFContainer.extraRounded({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.alignment,
|
||||
this.constraints,
|
||||
this.gradient,
|
||||
this.border,
|
||||
this.boxShadow,
|
||||
}) : borderRadius = SpacingTokens.radiusXl;
|
||||
|
||||
/// Container avec ombre
|
||||
UFContainer.elevated({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.alignment,
|
||||
this.constraints,
|
||||
this.gradient,
|
||||
this.border,
|
||||
}) : borderRadius = SpacingTokens.radiusLg,
|
||||
boxShadow = ShadowTokens.sm;
|
||||
|
||||
/// Container circulaire
|
||||
const UFContainer.circular({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.alignment,
|
||||
this.constraints,
|
||||
this.gradient,
|
||||
this.border,
|
||||
this.boxShadow,
|
||||
}) : borderRadius = SpacingTokens.radiusCircular;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: padding,
|
||||
margin: margin,
|
||||
alignment: alignment,
|
||||
constraints: constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: gradient == null ? (color ?? AppColors.lightSurface) : null,
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
border: border,
|
||||
boxShadow: boxShadow,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
127
lib/shared/design_system/components/uf_header.dart
Normal file
127
lib/shared/design_system/components/uf_header.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../unionflow_design_system.dart';
|
||||
|
||||
/// Header harmonisé UnionFlow
|
||||
///
|
||||
/// Composant header standardisé pour toutes les pages de l'application.
|
||||
/// Garantit la cohérence visuelle et l'expérience utilisateur.
|
||||
class UFHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final IconData icon;
|
||||
final List<Widget>? actions;
|
||||
final VoidCallback? onNotificationTap;
|
||||
final VoidCallback? onSettingsTap;
|
||||
final bool showActions;
|
||||
|
||||
const UFHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.icon,
|
||||
this.actions,
|
||||
this.onNotificationTap,
|
||||
this.onSettingsTap,
|
||||
this.showActions = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.xl),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primaryGreen, AppColors.brandGreenLight],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
|
||||
boxShadow: ShadowTokens.primary,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icône et contenu principal
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.lg),
|
||||
|
||||
// Titre et sous-titre
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
if (showActions) _buildActions(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActions() {
|
||||
if (actions != null) {
|
||||
return Row(children: actions!);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (onNotificationTap != null)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: onNotificationTap,
|
||||
icon: const Icon(
|
||||
Icons.notifications_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onNotificationTap != null && onSettingsTap != null)
|
||||
const SizedBox(width: SpacingTokens.sm),
|
||||
if (onSettingsTap != null)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: onSettingsTap,
|
||||
icon: const Icon(
|
||||
Icons.settings_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
237
lib/shared/design_system/components/uf_page_header.dart
Normal file
237
lib/shared/design_system/components/uf_page_header.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../unionflow_design_system.dart';
|
||||
|
||||
/// Header de page compact et moderne
|
||||
///
|
||||
/// Composant header minimaliste pour les pages principales du BottomNavigationBar.
|
||||
/// Design épuré sans gradient lourd, optimisé pour l'espace.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UFPageHeader(
|
||||
/// title: 'Membres',
|
||||
/// icon: Icons.people,
|
||||
/// actions: [
|
||||
/// IconButton(icon: Icon(Icons.add), onPressed: () => Navigator.pop(context)),
|
||||
/// ],
|
||||
/// )
|
||||
/// ```
|
||||
class UFPageHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final List<Widget>? actions;
|
||||
final Color? iconColor;
|
||||
final bool showDivider;
|
||||
|
||||
const UFPageHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.actions,
|
||||
this.iconColor,
|
||||
this.showDivider = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.lg,
|
||||
vertical: SpacingTokens.md,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icône
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveIconColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: effectiveIconColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
|
||||
// Titre
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
if (actions != null) ...actions!,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Divider optionnel
|
||||
if (showDivider)
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: AppColors.lightBorder,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Header de page avec statistiques
|
||||
///
|
||||
/// Header compact avec des métriques KPI intégrées.
|
||||
class UFPageHeaderWithStats extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final List<UFHeaderStat> stats;
|
||||
final List<Widget>? actions;
|
||||
final Color? iconColor;
|
||||
|
||||
const UFPageHeaderWithStats({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.stats,
|
||||
this.actions,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveIconColor = iconColor ?? AppColors.primaryGreen;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Titre et actions
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
SpacingTokens.lg,
|
||||
SpacingTokens.md,
|
||||
SpacingTokens.lg,
|
||||
SpacingTokens.sm,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icône
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveIconColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: effectiveIconColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.md),
|
||||
|
||||
// Titre
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textPrimaryLight,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
if (actions != null) ...actions!,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Statistiques
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
SpacingTokens.lg,
|
||||
0,
|
||||
SpacingTokens.lg,
|
||||
SpacingTokens.md,
|
||||
),
|
||||
child: Row(
|
||||
children: stats.map((stat) {
|
||||
final isLast = stat == stats.last;
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: isLast ? 0 : SpacingTokens.sm,
|
||||
),
|
||||
child: _buildStatItem(stat),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
|
||||
// Divider
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: AppColors.lightBorder,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(UFHeaderStat stat) {
|
||||
final effectiveColor = stat.color ?? AppColors.primaryGreen;
|
||||
return UFContainer.rounded(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.md,
|
||||
vertical: SpacingTokens.sm,
|
||||
),
|
||||
color: effectiveColor.withOpacity(0.05),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
stat.value,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: effectiveColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
stat.label,
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistique pour UFPageHeaderWithStats
|
||||
class UFHeaderStat {
|
||||
final String label;
|
||||
final String value;
|
||||
final Color? color;
|
||||
|
||||
const UFHeaderStat({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.color,
|
||||
});
|
||||
}
|
||||
|
||||
81
lib/shared/design_system/components/union_action_button.dart
Normal file
81
lib/shared/design_system/components/union_action_button.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Bouton d'action rapide UnionFlow
|
||||
class UnionActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final Color? backgroundColor;
|
||||
final Color? iconColor;
|
||||
|
||||
const UnionActionButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.backgroundColor,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? UnionFlowColors.unionGreenPale,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: (backgroundColor ?? UnionFlowColors.unionGreenPale)
|
||||
.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Grid d'actions rapides
|
||||
class UnionActionGrid extends StatelessWidget {
|
||||
final List<UnionActionButton> actions;
|
||||
|
||||
const UnionActionGrid({
|
||||
super.key,
|
||||
required this.actions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
for (int i = 0; i < actions.length; i++) ...[
|
||||
Expanded(child: actions[i]),
|
||||
if (i < actions.length - 1) const SizedBox(width: 12),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
98
lib/shared/design_system/components/union_balance_card.dart
Normal file
98
lib/shared/design_system/components/union_balance_card.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Card de balance UnionFlow - Affichage élégant du solde principal
|
||||
class UnionBalanceCard extends StatelessWidget {
|
||||
final String label;
|
||||
final String amount;
|
||||
final String? trend;
|
||||
final bool? isTrendPositive;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const UnionBalanceCard({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.amount,
|
||||
this.trend,
|
||||
this.isTrendPositive,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
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: 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
168
lib/shared/design_system/components/union_export_button.dart
Normal file
168
lib/shared/design_system/components/union_export_button.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Type d'export disponible
|
||||
enum ExportType {
|
||||
pdf('PDF', Icons.picture_as_pdf),
|
||||
excel('Excel', Icons.table_chart),
|
||||
csv('CSV', Icons.description);
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
const ExportType(this.label, this.icon);
|
||||
}
|
||||
|
||||
/// Bouton d'export avec options
|
||||
class UnionExportButton extends StatelessWidget {
|
||||
final Function(ExportType) onExport;
|
||||
final bool isLoading;
|
||||
|
||||
const UnionExportButton({
|
||||
super.key,
|
||||
required this.onExport,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<ExportType>(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: UnionFlowColors.greenGlowShadow,
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.download,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
itemBuilder: (context) => ExportType.values.map((type) {
|
||||
return PopupMenuItem<ExportType>(
|
||||
value: type,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
type.icon,
|
||||
size: 20,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Exporter en ${type.label}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onSelected: onExport,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 8,
|
||||
color: UnionFlowColors.surface,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog pour confirmer l'export
|
||||
class ExportConfirmDialog extends StatelessWidget {
|
||||
final ExportType exportType;
|
||||
final VoidCallback onConfirm;
|
||||
final String? message;
|
||||
|
||||
const ExportConfirmDialog({
|
||||
super.key,
|
||||
required this.exportType,
|
||||
required this.onConfirm,
|
||||
this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor: UnionFlowColors.surface,
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.unionGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
exportType.icon,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Exporter en ${exportType.label}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
message ?? 'Voulez-vous exporter le rapport au format ${exportType.label}?',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'Annuler',
|
||||
style: TextStyle(
|
||||
color: UnionFlowColors.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onConfirm();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'Confirmer',
|
||||
style: TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/shared/design_system/components/union_glass_card.dart
Normal file
65
lib/shared/design_system/components/union_glass_card.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Card avec effet glassmorphism
|
||||
class UnionGlassCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double? borderRadius;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const UnionGlassCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.borderRadius,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? 16),
|
||||
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),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
padding: padding ?? const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white.withOpacity(0.2),
|
||||
Colors.white.withOpacity(0.1),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
216
lib/shared/design_system/components/union_line_chart.dart
Normal file
216
lib/shared/design_system/components/union_line_chart.dart
Normal file
@@ -0,0 +1,216 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Graphique en ligne UnionFlow - Pour afficher l'évolution temporelle
|
||||
class UnionLineChart extends StatelessWidget {
|
||||
final List<FlSpot> spots;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Color? lineColor;
|
||||
final Color? gradientStartColor;
|
||||
final Color? gradientEndColor;
|
||||
|
||||
const UnionLineChart({
|
||||
super.key,
|
||||
required this.spots,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.lineColor,
|
||||
this.gradientStartColor,
|
||||
this.gradientEndColor,
|
||||
});
|
||||
|
||||
/// Calcule maxY de manière sécurisée pour éviter NaN, Infinity ou 0
|
||||
double _calculateSafeMaxY() {
|
||||
if (spots.isEmpty) return 100.0;
|
||||
|
||||
final maxValue = spots.map((e) => e.y).reduce((a, b) => a > b ? a : b);
|
||||
|
||||
// Si maxValue est invalide (NaN, Infinity) ou trop petit
|
||||
if (maxValue.isNaN || maxValue.isInfinite || maxValue <= 0) {
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
return maxValue * 1.2;
|
||||
}
|
||||
|
||||
/// Calcule maxX de manière sécurisée
|
||||
double _calculateSafeMaxX() {
|
||||
if (spots.isEmpty) return 11.0; // 12 mois - 1
|
||||
return spots.length.toDouble() - 1;
|
||||
}
|
||||
|
||||
/// Calcule l'intervalle de grille approprié basé sur maxY
|
||||
double _calculateGridInterval() {
|
||||
final maxY = _calculateSafeMaxY();
|
||||
|
||||
// Calculer un intervalle qui donne environ 4-6 lignes de grille
|
||||
final baseInterval = maxY / 5;
|
||||
|
||||
if (baseInterval == 0) return 20.0; // Fallback si maxY est trop petit
|
||||
|
||||
// Arrondir à un nombre "propre" (puissance de 10)
|
||||
final magnitude = pow(10.0, (log(baseInterval) / log(10.0)).floor()).toDouble();
|
||||
final normalized = baseInterval / magnitude;
|
||||
|
||||
// Arrondir vers le haut au multiple de 1, 2 ou 5 le plus proche
|
||||
double roundedInterval;
|
||||
if (normalized <= 1) {
|
||||
roundedInterval = 1;
|
||||
} else if (normalized <= 2) {
|
||||
roundedInterval = 2;
|
||||
} else if (normalized <= 5) {
|
||||
roundedInterval = 5;
|
||||
} else {
|
||||
roundedInterval = 10;
|
||||
}
|
||||
|
||||
return roundedInterval * magnitude;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveLineColor = lineColor ?? UnionFlowColors.unionGreen;
|
||||
final effectiveGradientStart = gradientStartColor ?? UnionFlowColors.unionGreen.withOpacity(0.3);
|
||||
final effectiveGradientEnd = gradientEndColor ?? UnionFlowColors.unionGreen.withOpacity(0.0);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Chart
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: _calculateGridInterval(),
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: UnionFlowColors.border.withOpacity(0.2),
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
interval: 1,
|
||||
getTitlesWidget: (value, meta) {
|
||||
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
|
||||
if (value.toInt() >= 0 && value.toInt() < months.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
months[value.toInt()],
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
'${(value / 1000).toStringAsFixed(0)}K',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
minX: 0,
|
||||
maxX: _calculateSafeMaxX(),
|
||||
minY: 0,
|
||||
maxY: _calculateSafeMaxY(),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: true,
|
||||
color: effectiveLineColor,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: UnionFlowColors.surface,
|
||||
strokeWidth: 2,
|
||||
strokeColor: effectiveLineColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
effectiveGradientStart,
|
||||
effectiveGradientEnd,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Badge de notification avec compteur
|
||||
class UnionNotificationBadge extends StatelessWidget {
|
||||
final int count;
|
||||
final Widget child;
|
||||
final Color? badgeColor;
|
||||
final bool showZero;
|
||||
|
||||
const UnionNotificationBadge({
|
||||
super.key,
|
||||
required this.count,
|
||||
required this.child,
|
||||
this.badgeColor,
|
||||
this.showZero = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shouldShow = count > 0 || showZero;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
child,
|
||||
if (shouldShow)
|
||||
Positioned(
|
||||
right: -6,
|
||||
top: -6,
|
||||
child: AnimatedScale(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
scale: 1.0,
|
||||
curve: Curves.elasticOut,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeColor ?? UnionFlowColors.error,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: UnionFlowColors.surface,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (badgeColor ?? UnionFlowColors.error)
|
||||
.withOpacity(0.4),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 18,
|
||||
minHeight: 18,
|
||||
),
|
||||
child: Text(
|
||||
count > 99 ? '99+' : count.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget de notification en temps réel (toast)
|
||||
class UnionNotificationToast extends StatelessWidget {
|
||||
final String title;
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const UnionNotificationToast({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.message,
|
||||
this.icon = Icons.notifications_active,
|
||||
this.color = UnionFlowColors.info,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
IconData icon = Icons.notifications_active,
|
||||
Color color = UnionFlowColors.info,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
final overlay = Overlay.of(context);
|
||||
late OverlayEntry entry;
|
||||
|
||||
entry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: 60,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
builder: (context, value, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, -20 * (1 - value)),
|
||||
child: Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: UnionNotificationToast(
|
||||
title: title,
|
||||
message: message,
|
||||
icon: icon,
|
||||
color: color,
|
||||
onTap: () {
|
||||
entry.remove();
|
||||
onTap?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(entry);
|
||||
|
||||
// Auto-dismiss après 4 secondes
|
||||
Future.delayed(const Duration(seconds: 4), () {
|
||||
entry.remove();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.mediumShadow,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: color,
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 18,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
111
lib/shared/design_system/components/union_period_filter.dart
Normal file
111
lib/shared/design_system/components/union_period_filter.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Filtre de période pour le dashboard
|
||||
enum PeriodFilter {
|
||||
today('Aujourd\'hui'),
|
||||
week('Cette semaine'),
|
||||
month('Ce mois'),
|
||||
quarter('Ce trimestre'),
|
||||
year('Cette année'),
|
||||
custom('Personnalisé');
|
||||
|
||||
final String label;
|
||||
const PeriodFilter(this.label);
|
||||
}
|
||||
|
||||
/// Widget de sélection de période
|
||||
class UnionPeriodFilter extends StatelessWidget {
|
||||
final PeriodFilter selectedPeriod;
|
||||
final Function(PeriodFilter) onPeriodChanged;
|
||||
final bool showCustom;
|
||||
|
||||
const UnionPeriodFilter({
|
||||
super.key,
|
||||
required this.selectedPeriod,
|
||||
required this.onPeriodChanged,
|
||||
this.showCustom = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final periods = showCustom
|
||||
? PeriodFilter.values
|
||||
: PeriodFilter.values.where((p) => p != PeriodFilter.custom).toList();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Période',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: periods.map((period) {
|
||||
final isSelected = selectedPeriod == period;
|
||||
return GestureDetector(
|
||||
onTap: () => onPeriodChanged(period),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: isSelected
|
||||
? UnionFlowColors.primaryGradient
|
||||
: null,
|
||||
color: isSelected
|
||||
? null
|
||||
: UnionFlowColors.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? UnionFlowColors.unionGreen
|
||||
: UnionFlowColors.border,
|
||||
width: isSelected ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
period.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
92
lib/shared/design_system/components/union_pie_chart.dart
Normal file
92
lib/shared/design_system/components/union_pie_chart.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
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
|
||||
class UnionPieChart extends StatelessWidget {
|
||||
final List<PieChartSectionData> sections;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final double? centerSpaceRadius;
|
||||
|
||||
const UnionPieChart({
|
||||
super.key,
|
||||
required this.sections,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.centerSpaceRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Chart
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: centerSpaceRadius ?? 50,
|
||||
sections: sections,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper pour créer des sections de pie chart
|
||||
class UnionPieChartSection {
|
||||
static PieChartSectionData create({
|
||||
required double value,
|
||||
required Color color,
|
||||
required String title,
|
||||
double radius = 50,
|
||||
bool showTitle = true,
|
||||
}) {
|
||||
return PieChartSectionData(
|
||||
color: color,
|
||||
value: value,
|
||||
title: showTitle ? title : '',
|
||||
radius: radius,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
badgeWidget: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
100
lib/shared/design_system/components/union_progress_card.dart
Normal file
100
lib/shared/design_system/components/union_progress_card.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Card de progression UnionFlow avec barre de progrès élégante
|
||||
class UnionProgressCard extends StatelessWidget {
|
||||
final String title;
|
||||
final double progress; // 0.0 à 1.0
|
||||
final String subtitle;
|
||||
final Color? progressColor;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const UnionProgressCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.progress,
|
||||
required this.subtitle,
|
||||
this.progressColor,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveColor = progressColor ?? UnionFlowColors.gold;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Progress bar
|
||||
Stack(
|
||||
children: [
|
||||
// Background track
|
||||
Container(
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.border,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
// Progress fill avec gradient
|
||||
FractionallySizedBox(
|
||||
widthFactor: progress.clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
effectiveColor,
|
||||
effectiveColor.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: effectiveColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Subtitle
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
105
lib/shared/design_system/components/union_stat_widget.dart
Normal file
105
lib/shared/design_system/components/union_stat_widget.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Widget de statistique compacte avec icône et tendance
|
||||
class UnionStatWidget extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String? trend;
|
||||
final bool? isTrendUp;
|
||||
|
||||
const UnionStatWidget({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.trend,
|
||||
this.isTrendUp,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: color,
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
Text(
|
||||
trend!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isTrendUp == true
|
||||
? UnionFlowColors.success
|
||||
: UnionFlowColors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
199
lib/shared/design_system/components/union_transaction_tile.dart
Normal file
199
lib/shared/design_system/components/union_transaction_tile.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Tuile de transaction UnionFlow
|
||||
class UnionTransactionTile extends StatelessWidget {
|
||||
final String name;
|
||||
final String amount;
|
||||
final String status;
|
||||
final String? date;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const UnionTransactionTile({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.amount,
|
||||
required this.status,
|
||||
this.date,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
Color _getStatusColor() {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'confirmé':
|
||||
case 'confirmed':
|
||||
return UnionFlowColors.success;
|
||||
case 'en attente':
|
||||
case 'pending':
|
||||
return UnionFlowColors.warning;
|
||||
case 'échoué':
|
||||
case 'failed':
|
||||
return UnionFlowColors.error;
|
||||
default:
|
||||
return UnionFlowColors.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: UnionFlowColors.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Avatar avec initiale
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
name.isNotEmpty ? name[0].toUpperCase() : '?',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Nom et date
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
if (date != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
date!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: UnionFlowColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Montant et status
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
amount,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor().withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _getStatusColor(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Liste de transactions dans une card
|
||||
class UnionTransactionCard extends StatelessWidget {
|
||||
final String title;
|
||||
final List<UnionTransactionTile> transactions;
|
||||
final VoidCallback? onSeeAll;
|
||||
|
||||
const UnionTransactionCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.transactions,
|
||||
this.onSeeAll,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
if (onSeeAll != null)
|
||||
GestureDetector(
|
||||
onTap: onSeeAll,
|
||||
child: const Text(
|
||||
'Voir tout',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Transactions
|
||||
...transactions,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../tokens/unionflow_colors.dart';
|
||||
|
||||
/// Carte premium affichant la vue unifiée "Compte Adhérent" du membre.
|
||||
class UnionUnifiedAccountCard extends StatelessWidget {
|
||||
final String numeroMembre;
|
||||
final String organisationNom;
|
||||
final String soldeTotal;
|
||||
final String capaciteEmprunt;
|
||||
final String epargneBloquee;
|
||||
final double engagementRate; // 0.0 to 1.0
|
||||
final VoidCallback? onDetailsTap;
|
||||
|
||||
const UnionUnifiedAccountCard({
|
||||
super.key,
|
||||
required this.numeroMembre,
|
||||
required this.organisationNom,
|
||||
required this.soldeTotal,
|
||||
required this.capaciteEmprunt,
|
||||
required this.epargneBloquee,
|
||||
required this.engagementRate,
|
||||
this.onDetailsTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
UnionFlowColors.unionGreen,
|
||||
UnionFlowColors.unionGreen.withOpacity(0.85),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UnionFlowColors.unionGreen.withOpacity(0.35),
|
||||
offset: const Offset(0, 10),
|
||||
blurRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Pattern décoratif subtil (fond)
|
||||
Positioned(
|
||||
right: -20,
|
||||
bottom: -20,
|
||||
child: Icon(
|
||||
Icons.account_balance_wallet,
|
||||
size: 150,
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête : Organisation + Numéro
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
organisationNom.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'COMPTE ADHÉRENT',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white30),
|
||||
),
|
||||
child: Text(
|
||||
numeroMembre,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: 'Courier', // Look like a card number
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Solde Total Disponible
|
||||
const Text(
|
||||
'Solde Total Disponible',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
soldeTotal,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Grille de détails
|
||||
Row(
|
||||
children: [
|
||||
_buildSubStat('Capacité Emprunt', capaciteEmprunt, Icons.rocket_launch, UnionFlowColors.gold),
|
||||
const SizedBox(width: 16),
|
||||
_buildSubStat('Épargne Bloquée', epargneBloquee, Icons.lock_clock, Colors.white60),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Barre d'engagement
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Taux d\'engagement (Cotisations)',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
'${(engagementRate * 100).toInt()}%',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w800),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: engagementRate,
|
||||
backgroundColor: Colors.white10,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(UnionFlowColors.gold),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubStat(String label, String value, IconData icon, Color iconColor) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 12, color: iconColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user