Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 deletions

View File

@@ -0,0 +1,349 @@
# Guide du Design System UnionFlow
## 📋 Table des matières
1. [Introduction](#introduction)
2. [Tokens](#tokens)
3. [Composants](#composants)
4. [Bonnes pratiques](#bonnes-pratiques)
---
## Introduction
Le Design System UnionFlow garantit la cohérence visuelle et l'expérience utilisateur dans toute l'application.
**Palette de couleurs** : Bleu Roi (#4169E1) + Bleu Pétrole (#2C5F6F)
**Basé sur** : Material Design 3 et tendances UI/UX 2024-2025
### Import
```dart
import 'package:unionflow_mobile_apps/shared/design_system/unionflow_design_system.dart';
```
---
## Tokens
### 🎨 Couleurs (ColorTokens)
```dart
// Primaire
ColorTokens.primary // Bleu Roi #4169E1
ColorTokens.onPrimary // Blanc #FFFFFF
ColorTokens.primaryContainer // Container bleu roi
// Sémantiques
ColorTokens.success // Vert #10B981
ColorTokens.error // Rouge #DC2626
ColorTokens.warning // Orange #F59E0B
ColorTokens.info // Bleu #0EA5E9
// Surfaces
ColorTokens.surface // Blanc #FFFFFF
ColorTokens.background // Gris clair #F8F9FA
ColorTokens.onSurface // Texte principal #1F2937
ColorTokens.onSurfaceVariant // Texte secondaire #6B7280
// Gradients
ColorTokens.primaryGradient // [Bleu Roi, Bleu Roi clair]
```
### 📏 Espacements (SpacingTokens)
```dart
SpacingTokens.xs // 2px
SpacingTokens.sm // 4px
SpacingTokens.md // 8px
SpacingTokens.lg // 12px
SpacingTokens.xl // 16px
SpacingTokens.xxl // 20px
SpacingTokens.xxxl // 24px
SpacingTokens.huge // 32px
SpacingTokens.giant // 48px
```
### 🔘 Rayons (SpacingTokens)
```dart
SpacingTokens.radiusXs // 2px
SpacingTokens.radiusSm // 4px
SpacingTokens.radiusMd // 8px
SpacingTokens.radiusLg // 12px - Standard pour cards
SpacingTokens.radiusXl // 16px
SpacingTokens.radiusXxl // 20px
SpacingTokens.radiusCircular // 999px - Boutons ronds
```
### 🌑 Ombres (ShadowTokens)
```dart
ShadowTokens.xs // Ombre minimale
ShadowTokens.sm // Ombre petite (cards, boutons)
ShadowTokens.md // Ombre moyenne (cards importantes)
ShadowTokens.lg // Ombre large (modals, dialogs)
ShadowTokens.xl // Ombre très large (éléments flottants)
// Ombres colorées
ShadowTokens.primary // Ombre avec couleur primaire
ShadowTokens.success // Ombre verte
ShadowTokens.error // Ombre rouge
```
### ✍️ Typographie (TypographyTokens)
```dart
TypographyTokens.displayLarge // 57px - Titres héroïques
TypographyTokens.headlineLarge // 32px - Titres de page
TypographyTokens.headlineMedium // 28px - Sous-titres
TypographyTokens.titleLarge // 22px - Titres de section
TypographyTokens.titleMedium // 16px - Titres de card
TypographyTokens.bodyLarge // 16px - Corps de texte
TypographyTokens.bodyMedium // 14px - Corps standard
TypographyTokens.labelLarge // 14px - Labels
TypographyTokens.labelSmall // 11px - Petits labels
```
---
## Composants
### 📦 UFCard - Card standardisé
```dart
// Card avec ombre (par défaut)
UFCard(
child: Text('Contenu'),
)
// Card avec bordure
UFCard.outlined(
borderColor: ColorTokens.primary,
child: Text('Contenu'),
)
// Card avec fond coloré
UFCard.filled(
color: ColorTokens.primaryContainer,
child: Text('Contenu'),
)
// Card cliquable
UFCard(
onTap: () => print('Cliqué'),
child: Text('Contenu'),
)
```
### 📦 UFContainer - Container standardisé
```dart
// Container standard
UFContainer(
child: Text('Contenu'),
)
// Container arrondi
UFContainer.rounded(
color: ColorTokens.primary,
padding: EdgeInsets.all(SpacingTokens.lg),
child: Text('Contenu'),
)
// Container avec ombre
UFContainer.elevated(
child: Text('Contenu'),
)
// Container circulaire
UFContainer.circular(
width: 48,
height: 48,
color: ColorTokens.primary,
child: Icon(Icons.person),
)
```
### 📊 UFStatCard - Card de statistiques
```dart
UFStatCard(
title: 'Membres',
value: '142',
icon: Icons.people,
iconColor: ColorTokens.primary,
subtitle: '+5 ce mois',
onTap: () => navigateToMembers(),
)
```
### UFInfoCard - Card d'information
```dart
UFInfoCard(
title: 'État du système',
icon: Icons.health_and_safety,
iconColor: ColorTokens.success,
trailing: Badge(label: Text('OK')),
child: Column(
children: [
Text('Tous les systèmes fonctionnent normalement'),
],
),
)
```
### 🎯 UFHeader - Header de page
```dart
UFHeader(
title: 'Tableau de bord',
subtitle: 'Vue d\'ensemble de votre activité',
icon: Icons.dashboard,
onNotificationTap: () => showNotifications(),
onSettingsTap: () => showSettings(),
)
```
### 📱 UFAppBar - AppBar standardisé
```dart
UFAppBar(
title: 'Détails du membre',
actions: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () => edit(),
),
],
)
```
### 🔘 Boutons
```dart
// Bouton primaire
UFPrimaryButton(
text: 'Enregistrer',
onPressed: () => save(),
icon: Icons.save,
)
// Bouton secondaire
UFSecondaryButton(
text: 'Annuler',
onPressed: () => cancel(),
)
```
---
## Bonnes pratiques
### ✅ À FAIRE
```dart
// ✅ Utiliser les tokens
Container(
padding: EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.sm,
),
)
// ✅ Utiliser les composants
UFCard(
child: Text('Contenu'),
)
```
### ❌ À ÉVITER
```dart
// ❌ Valeurs hardcodées
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
)
// ❌ Card Flutter standard
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Text('Contenu'),
)
```
### 📐 Hiérarchie des espacements
- **xs/sm** : Éléments très proches (icône + texte)
- **md/lg** : Espacement interne standard
- **xl/xxl** : Espacement entre sections
- **xxxl+** : Grandes séparations
### 🎨 Hiérarchie des couleurs
1. **primary** : Actions principales, navigation active
2. **secondary** : Actions secondaires
3. **success/error/warning** : États et feedbacks
4. **surface/background** : Fonds et containers
### 🌑 Hiérarchie des ombres
- **xs/sm** : Cards et boutons standards
- **md** : Cards importantes
- **lg/xl** : Modals, dialogs, éléments flottants
- **Colorées** : Éléments avec accent visuel
---
## 🔄 Migration
Pour migrer du code existant :
1. Remplacer `Card` par `UFCard`
2. Remplacer `Container` personnalisés par `UFContainer`
3. Remplacer couleurs hardcodées par `ColorTokens`
4. Remplacer espacements hardcodés par `SpacingTokens`
5. Remplacer ombres personnalisées par `ShadowTokens`
**Exemple** :
```dart
// Avant
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Contenu'),
),
)
// Après
UFCard(
child: Text('Contenu'),
)
```
---
**Version** : 1.0.0
**Dernière mise à jour** : 2025-10-05

View File

@@ -0,0 +1,103 @@
/// 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 '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.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;
const UFPrimaryButton({
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: ColorTokens.primary, // Bleu roi
foregroundColor: ColorTokens.onPrimary, // Blanc
disabledBackgroundColor: ColorTokens.primary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onPrimary.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
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>(
ColorTokens.onPrimary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: TypographyTokens.buttonLarge,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,82 @@
/// 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 '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.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: ColorTokens.secondary, // Indigo
foregroundColor: ColorTokens.onSecondary, // Blanc
disabledBackgroundColor: ColorTokens.secondary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onSecondary.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
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>(
ColorTokens.onSecondary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: TypographyTokens.buttonLarge,
),
],
),
),
);
}
}

View 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 ?? ColorTokens.surface,
borderRadius: BorderRadius.circular(radius),
boxShadow: elevation != null
? [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: elevation!,
offset: const Offset(0, 2),
),
]
: ShadowTokens.sm,
);
case UFCardStyle.outlined:
return BoxDecoration(
color: color ?? ColorTokens.surface,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: borderColor ?? ColorTokens.outline,
width: borderWidth ?? 1.0,
),
);
case UFCardStyle.filled:
return BoxDecoration(
color: color ?? ColorTokens.surfaceContainer,
borderRadius: BorderRadius.circular(radius),
);
}
}
}
/// Styles de Card disponibles
enum UFCardStyle {
/// Card avec ombre
elevated,
/// Card avec bordure
outlined,
/// Card avec fond coloré
filled,
}

View File

@@ -0,0 +1,92 @@
/// 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 '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../tokens/shadow_tokens.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 ?? ColorTokens.primary;
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.xl);
return Container(
padding: effectivePadding,
decoration: BoxDecoration(
color: ColorTokens.surface,
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: TypographyTokens.titleMedium.copyWith(
color: ColorTokens.onSurface,
),
),
),
if (trailing != null) trailing!,
],
),
const SizedBox(height: SpacingTokens.xl),
// Contenu
child,
],
),
);
}
}

View File

@@ -0,0 +1,76 @@
/// 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 '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.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: TypographyTokens.labelSmall.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
Text(
label,
style: TypographyTokens.labelSmall.copyWith(
fontSize: 9,
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -0,0 +1,143 @@
/// 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 '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.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 ?? ColorTokens.primary;
final effectiveIconBgColor = iconBackgroundColor ??
effectiveIconColor.withOpacity(0.1);
return Card(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
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: ColorTokens.onSurfaceVariant,
),
],
),
const SizedBox(height: SpacingTokens.lg),
// Titre
Text(
title,
style: TypographyTokens.labelLarge.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
const SizedBox(height: SpacingTokens.sm),
// Valeur
Text(
value,
style: TypographyTokens.cardValue.copyWith(
color: ColorTokens.onSurface,
),
),
// Sous-titre optionnel
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.sm),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
],
),
),
),
);
}
}

View 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';
// TODO: Ajouter d'autres composants au fur et à mesure
// export 'buttons/uf_outline_button.dart';
// export 'buttons/uf_text_button.dart';
// export 'cards/uf_event_card.dart';
// export 'inputs/uf_text_field.dart';

View File

@@ -0,0 +1,99 @@
/// 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 '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.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 ?? ColorTokens.surfaceVariant;
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: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: ColorTokens.outline),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
value: value,
onChanged: onChanged,
items: items.map((item) {
return DropdownMenuItem<T>(
value: item,
child: Text(effectiveItemBuilder(item)),
);
}).toList(),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,90 @@
/// 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 '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.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 ?? ColorTokens.surfaceVariant;
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: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
),
),
Text(
subtitle,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: ColorTokens.primary,
),
],
),
);
}
}

View File

@@ -0,0 +1,60 @@
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.
class UFAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool automaticallyImplyLeading;
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.bottom,
this.backgroundColor,
this.foregroundColor,
this.elevation = 0,
});
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(title),
backgroundColor: backgroundColor ?? ColorTokens.primary,
foregroundColor: foregroundColor ?? ColorTokens.onPrimary,
elevation: elevation,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
actions: actions,
bottom: bottom,
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light, // Icônes claires sur fond bleu
statusBarBrightness: Brightness.dark, // Pour iOS
),
centerTitle: false,
titleTextStyle: TypographyTokens.titleLarge.copyWith(
color: foregroundColor ?? ColorTokens.onPrimary,
fontWeight: FontWeight.w600,
),
);
}
@override
Size get preferredSize => Size.fromHeight(
kToolbarHeight + (bottom?.preferredSize.height ?? 0.0),
);
}

View 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 ?? ColorTokens.surface) : null,
gradient: gradient,
borderRadius: BorderRadius.circular(borderRadius),
border: border,
boxShadow: boxShadow,
),
child: child,
);
}
}

View 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: ColorTokens.primaryGradient,
),
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: ColorTokens.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: ColorTokens.onPrimary,
size: 24,
),
),
const SizedBox(width: SpacingTokens.lg),
// Titre et sous-titre
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onPrimary.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: ColorTokens.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onNotificationTap,
icon: const Icon(
Icons.notifications_outlined,
color: ColorTokens.onPrimary,
),
),
),
if (onNotificationTap != null && onSettingsTap != null)
const SizedBox(width: SpacingTokens.sm),
if (onSettingsTap != null)
Container(
decoration: BoxDecoration(
color: ColorTokens.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onSettingsTap,
icon: const Icon(
Icons.settings_outlined,
color: ColorTokens.onPrimary,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,248 @@
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: () {}),
/// ],
/// )
/// ```
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 ?? ColorTokens.primary;
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: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
),
// Actions
if (actions != null) ...actions!,
],
),
),
// Divider optionnel
if (showDivider)
Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.1),
),
],
);
}
}
/// Header de page avec statistiques
///
/// Header compact avec des métriques KPI intégrées.
///
/// Usage:
/// ```dart
/// UFPageHeaderWithStats(
/// title: 'Membres',
/// icon: Icons.people,
/// stats: [
/// UFHeaderStat(label: 'Total', value: '142'),
/// UFHeaderStat(label: 'Actifs', value: '128'),
/// ],
/// )
/// ```
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 ?? ColorTokens.primary;
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: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
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
Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.1),
),
],
);
}
Widget _buildStatItem(UFHeaderStat stat) {
return UFContainer.rounded(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
color: (stat.color ?? ColorTokens.primary).withOpacity(0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stat.value,
style: TypographyTokens.titleMedium.copyWith(
color: stat.color ?? ColorTokens.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
stat.label,
style: TypographyTokens.labelSmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
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,
});
}

View File

@@ -0,0 +1,246 @@
import 'package:flutter/material.dart';
/// Design System pour les dashboards avec thème bleu roi et bleu pétrole
class DashboardTheme {
// === COULEURS PRINCIPALES ===
/// Bleu roi - Couleur principale
static const Color royalBlue = Color(0xFF4169E1);
/// Bleu pétrole - Couleur secondaire
static const Color tealBlue = Color(0xFF008B8B);
/// Variations du bleu roi
static const Color royalBlueLight = Color(0xFF6495ED);
static const Color royalBlueDark = Color(0xFF191970);
/// Variations du bleu pétrole
static const Color tealBlueLight = Color(0xFF20B2AA);
static const Color tealBlueDark = Color(0xFF006666);
// === COULEURS FONCTIONNELLES ===
/// Couleurs de statut
static const Color success = Color(0xFF10B981);
static const Color warning = Color(0xFFF59E0B);
static const Color error = Color(0xFFEF4444);
static const Color info = Color(0xFF3B82F6);
/// Couleurs neutres
static const Color white = Color(0xFFFFFFFF);
static const Color grey50 = Color(0xFFF9FAFB);
static const Color grey100 = Color(0xFFF3F4F6);
static const Color grey200 = Color(0xFFE5E7EB);
static const Color grey300 = Color(0xFFD1D5DB);
static const Color grey400 = Color(0xFF9CA3AF);
static const Color grey500 = Color(0xFF6B7280);
static const Color grey600 = Color(0xFF4B5563);
static const Color grey700 = Color(0xFF374151);
static const Color grey800 = Color(0xFF1F2937);
static const Color grey900 = Color(0xFF111827);
// === GRADIENTS ===
/// Gradient principal (bleu roi vers bleu pétrole)
static const LinearGradient primaryGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [royalBlue, tealBlue],
);
/// Gradient léger pour les cartes
static const LinearGradient cardGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [royalBlueLight, tealBlueLight],
stops: [0.0, 1.0],
);
/// Gradient sombre pour les headers
static const LinearGradient headerGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [royalBlueDark, royalBlue],
);
// === OMBRES ===
/// Ombre légère pour les cartes
static const List<BoxShadow> cardShadow = [
BoxShadow(
color: Color(0x1A000000),
blurRadius: 8,
offset: Offset(0, 2),
),
];
/// Ombre plus prononcée pour les éléments flottants
static const List<BoxShadow> elevatedShadow = [
BoxShadow(
color: Color(0x1F000000),
blurRadius: 16,
offset: Offset(0, 4),
),
];
/// Ombre subtile pour les éléments délicats
static const List<BoxShadow> subtleShadow = [
BoxShadow(
color: Color(0x0A000000),
blurRadius: 4,
offset: Offset(0, 1),
),
];
// === BORDURES ===
/// Rayon de bordure standard
static const double borderRadius = 12.0;
static const double borderRadiusSmall = 8.0;
static const double borderRadiusLarge = 16.0;
/// Bordures colorées
static const BorderSide primaryBorder = BorderSide(
color: royalBlue,
width: 1.0,
);
static const BorderSide secondaryBorder = BorderSide(
color: tealBlue,
width: 1.0,
);
// === ESPACEMENTS ===
static const double spacing2 = 2.0;
static const double spacing4 = 4.0;
static const double spacing6 = 6.0;
static const double spacing8 = 8.0;
static const double spacing12 = 12.0;
static const double spacing16 = 16.0;
static const double spacing20 = 20.0;
static const double spacing24 = 24.0;
static const double spacing32 = 32.0;
static const double spacing48 = 48.0;
// === STYLES DE TEXTE ===
/// Titre principal
static const TextStyle titleLarge = TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: grey900,
height: 1.2,
);
/// Titre de section
static const TextStyle titleMedium = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: grey800,
height: 1.3,
);
/// Titre de carte
static const TextStyle titleSmall = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: grey700,
height: 1.4,
);
/// Corps de texte
static const TextStyle bodyLarge = TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: grey700,
height: 1.5,
);
/// Corps de texte moyen
static const TextStyle bodyMedium = TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: grey600,
height: 1.4,
);
/// Petit texte
static const TextStyle bodySmall = TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: grey500,
height: 1.3,
);
/// Texte de métrique (gros chiffres)
static const TextStyle metricLarge = TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: royalBlue,
height: 1.1,
);
/// Texte de métrique moyen
static const TextStyle metricMedium = TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: tealBlue,
height: 1.2,
);
// === STYLES DE BOUTONS ===
/// Style de bouton principal
static ButtonStyle get primaryButtonStyle => ElevatedButton.styleFrom(
backgroundColor: royalBlue,
foregroundColor: white,
elevation: 2,
shadowColor: royalBlue.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: const EdgeInsets.symmetric(
horizontal: spacing20,
vertical: spacing12,
),
);
/// Style de bouton secondaire
static ButtonStyle get secondaryButtonStyle => OutlinedButton.styleFrom(
foregroundColor: tealBlue,
side: const BorderSide(color: tealBlue),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: const EdgeInsets.symmetric(
horizontal: spacing20,
vertical: spacing12,
),
);
// === DÉCORATION DE CONTENEURS ===
/// Décoration de carte standard
static BoxDecoration get cardDecoration => BoxDecoration(
color: white,
borderRadius: BorderRadius.circular(borderRadius),
boxShadow: cardShadow,
);
/// Décoration de carte avec gradient
static BoxDecoration get gradientCardDecoration => BoxDecoration(
gradient: cardGradient,
borderRadius: BorderRadius.circular(borderRadius),
boxShadow: cardShadow,
);
/// Décoration de header
static BoxDecoration get headerDecoration => const BoxDecoration(
gradient: headerGradient,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(borderRadiusLarge),
bottomRight: Radius.circular(borderRadiusLarge),
),
);
}

View File

@@ -0,0 +1,337 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Gestionnaire de thèmes personnalisables pour le Dashboard
class DashboardThemeManager {
static const String _themeKey = 'dashboard_theme';
static DashboardThemeData _currentTheme = DashboardThemeData.royalTeal();
static SharedPreferences? _prefs;
/// Initialise le gestionnaire de thèmes
static Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
await _loadSavedTheme();
}
/// Charge le thème sauvegardé
static Future<void> _loadSavedTheme() async {
final themeName = _prefs?.getString(_themeKey) ?? 'royalTeal';
_currentTheme = _getThemeByName(themeName);
}
/// Obtient le thème actuel
static DashboardThemeData get currentTheme => _currentTheme;
/// Change le thème et le sauvegarde
static Future<void> setTheme(String themeName) async {
_currentTheme = _getThemeByName(themeName);
await _prefs?.setString(_themeKey, themeName);
}
/// Obtient un thème par son nom
static DashboardThemeData _getThemeByName(String name) {
switch (name) {
case 'royalTeal':
return DashboardThemeData.royalTeal();
case 'oceanBlue':
return DashboardThemeData.oceanBlue();
case 'forestGreen':
return DashboardThemeData.forestGreen();
case 'sunsetOrange':
return DashboardThemeData.sunsetOrange();
case 'purpleNight':
return DashboardThemeData.purpleNight();
case 'darkMode':
return DashboardThemeData.darkMode();
default:
return DashboardThemeData.royalTeal();
}
}
/// Obtient la liste des thèmes disponibles
static List<ThemeOption> get availableThemes => [
ThemeOption('royalTeal', 'Bleu Roi & Pétrole', DashboardThemeData.royalTeal()),
ThemeOption('oceanBlue', 'Bleu Océan', DashboardThemeData.oceanBlue()),
ThemeOption('forestGreen', 'Vert Forêt', DashboardThemeData.forestGreen()),
ThemeOption('sunsetOrange', 'Orange Coucher', DashboardThemeData.sunsetOrange()),
ThemeOption('purpleNight', 'Violet Nuit', DashboardThemeData.purpleNight()),
ThemeOption('darkMode', 'Mode Sombre', DashboardThemeData.darkMode()),
];
}
/// Option de thème
class ThemeOption {
final String key;
final String name;
final DashboardThemeData theme;
const ThemeOption(this.key, this.name, this.theme);
}
/// Données d'un thème de dashboard
class DashboardThemeData {
final String name;
final Color primaryColor;
final Color secondaryColor;
final Color primaryLight;
final Color primaryDark;
final Color secondaryLight;
final Color secondaryDark;
final Color backgroundColor;
final Color surfaceColor;
final Color cardColor;
final Color textPrimary;
final Color textSecondary;
final Color success;
final Color warning;
final Color error;
final Color info;
final bool isDark;
const DashboardThemeData({
required this.name,
required this.primaryColor,
required this.secondaryColor,
required this.primaryLight,
required this.primaryDark,
required this.secondaryLight,
required this.secondaryDark,
required this.backgroundColor,
required this.surfaceColor,
required this.cardColor,
required this.textPrimary,
required this.textSecondary,
required this.success,
required this.warning,
required this.error,
required this.info,
this.isDark = false,
});
/// Thème Bleu Roi & Pétrole (par défaut)
factory DashboardThemeData.royalTeal() {
return const DashboardThemeData(
name: 'Bleu Roi & Pétrole',
primaryColor: Color(0xFF4169E1),
secondaryColor: Color(0xFF008B8B),
primaryLight: Color(0xFF6A8EF7),
primaryDark: Color(0xFF2E4BC6),
secondaryLight: Color(0xFF20B2AA),
secondaryDark: Color(0xFF006666),
backgroundColor: Color(0xFFF9FAFB),
surfaceColor: Color(0xFFFFFFFF),
cardColor: Color(0xFFFFFFFF),
textPrimary: Color(0xFF111827),
textSecondary: Color(0xFF6B7280),
success: Color(0xFF10B981),
warning: Color(0xFFF59E0B),
error: Color(0xFFEF4444),
info: Color(0xFF3B82F6),
);
}
/// Thème Bleu Océan
factory DashboardThemeData.oceanBlue() {
return const DashboardThemeData(
name: 'Bleu Océan',
primaryColor: Color(0xFF0EA5E9),
secondaryColor: Color(0xFF0284C7),
primaryLight: Color(0xFF38BDF8),
primaryDark: Color(0xFF0369A1),
secondaryLight: Color(0xFF0EA5E9),
secondaryDark: Color(0xFF075985),
backgroundColor: Color(0xFFF0F9FF),
surfaceColor: Color(0xFFFFFFFF),
cardColor: Color(0xFFFFFFFF),
textPrimary: Color(0xFF0C4A6E),
textSecondary: Color(0xFF64748B),
success: Color(0xFF059669),
warning: Color(0xFFD97706),
error: Color(0xFFDC2626),
info: Color(0xFF2563EB),
);
}
/// Thème Vert Forêt
factory DashboardThemeData.forestGreen() {
return const DashboardThemeData(
name: 'Vert Forêt',
primaryColor: Color(0xFF059669),
secondaryColor: Color(0xFF047857),
primaryLight: Color(0xFF10B981),
primaryDark: Color(0xFF065F46),
secondaryLight: Color(0xFF059669),
secondaryDark: Color(0xFF064E3B),
backgroundColor: Color(0xFFF0FDF4),
surfaceColor: Color(0xFFFFFFFF),
cardColor: Color(0xFFFFFFFF),
textPrimary: Color(0xFF064E3B),
textSecondary: Color(0xFF6B7280),
success: Color(0xFF10B981),
warning: Color(0xFFF59E0B),
error: Color(0xFFEF4444),
info: Color(0xFF3B82F6),
);
}
/// Thème Orange Coucher de Soleil
factory DashboardThemeData.sunsetOrange() {
return const DashboardThemeData(
name: 'Orange Coucher',
primaryColor: Color(0xFFEA580C),
secondaryColor: Color(0xFFDC2626),
primaryLight: Color(0xFFF97316),
primaryDark: Color(0xFFC2410C),
secondaryLight: Color(0xFFEF4444),
secondaryDark: Color(0xFFB91C1C),
backgroundColor: Color(0xFFFFF7ED),
surfaceColor: Color(0xFFFFFFFF),
cardColor: Color(0xFFFFFFFF),
textPrimary: Color(0xFF9A3412),
textSecondary: Color(0xFF78716C),
success: Color(0xFF059669),
warning: Color(0xFFF59E0B),
error: Color(0xFFDC2626),
info: Color(0xFF2563EB),
);
}
/// Thème Violet Nuit
factory DashboardThemeData.purpleNight() {
return const DashboardThemeData(
name: 'Violet Nuit',
primaryColor: Color(0xFF7C3AED),
secondaryColor: Color(0xFF9333EA),
primaryLight: Color(0xFF8B5CF6),
primaryDark: Color(0xFF5B21B6),
secondaryLight: Color(0xFFA855F7),
secondaryDark: Color(0xFF7E22CE),
backgroundColor: Color(0xFFFAF5FF),
surfaceColor: Color(0xFFFFFFFF),
cardColor: Color(0xFFFFFFFF),
textPrimary: Color(0xFF581C87),
textSecondary: Color(0xFF6B7280),
success: Color(0xFF059669),
warning: Color(0xFFF59E0B),
error: Color(0xFFEF4444),
info: Color(0xFF3B82F6),
);
}
/// Thème Mode Sombre
factory DashboardThemeData.darkMode() {
return const DashboardThemeData(
name: 'Mode Sombre',
primaryColor: Color(0xFF60A5FA),
secondaryColor: Color(0xFF34D399),
primaryLight: Color(0xFF93C5FD),
primaryDark: Color(0xFF3B82F6),
secondaryLight: Color(0xFF6EE7B7),
secondaryDark: Color(0xFF10B981),
backgroundColor: Color(0xFF111827),
surfaceColor: Color(0xFF1F2937),
cardColor: Color(0xFF374151),
textPrimary: Color(0xFFF9FAFB),
textSecondary: Color(0xFFD1D5DB),
success: Color(0xFF34D399),
warning: Color(0xFFFBBF24),
error: Color(0xFFF87171),
info: Color(0xFF60A5FA),
isDark: true,
);
}
/// Gradient primaire
LinearGradient get primaryGradient => LinearGradient(
colors: [primaryColor, secondaryColor],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// Gradient de carte
LinearGradient get cardGradient => LinearGradient(
colors: [
cardColor,
isDark ? surfaceColor : const Color(0xFFF8FAFC),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
/// Gradient d'en-tête
LinearGradient get headerGradient => LinearGradient(
colors: [primaryColor, primaryDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// Style de bouton primaire
ButtonStyle get primaryButtonStyle => ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: isDark ? textPrimary : Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
);
/// Style de bouton secondaire
ButtonStyle get secondaryButtonStyle => OutlinedButton.styleFrom(
foregroundColor: primaryColor,
side: BorderSide(color: primaryColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
);
/// Thème Flutter complet
ThemeData get flutterTheme => ThemeData(
useMaterial3: true,
brightness: isDark ? Brightness.dark : Brightness.light,
primaryColor: primaryColor,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: isDark ? Brightness.dark : Brightness.light,
secondary: secondaryColor,
surface: surfaceColor,
background: backgroundColor,
),
scaffoldBackgroundColor: backgroundColor,
cardColor: cardColor,
appBarTheme: AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: isDark ? textPrimary : Colors.white,
elevation: 0,
centerTitle: true,
),
elevatedButtonTheme: ElevatedButtonThemeData(style: primaryButtonStyle),
outlinedButtonTheme: OutlinedButtonThemeData(style: secondaryButtonStyle),
cardTheme: CardTheme(
color: cardColor,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
textTheme: TextTheme(
displayLarge: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: textPrimary,
),
displayMedium: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: textPrimary,
),
bodyLarge: TextStyle(
fontSize: 16,
color: textPrimary,
),
bodyMedium: TextStyle(
fontSize: 14,
color: textSecondary,
),
),
);
}

View File

@@ -0,0 +1,457 @@
/// Thème Sophistiqué UnionFlow
///
/// Implémentation complète du design system avec les dernières tendances UI/UX 2024-2025
/// Architecture modulaire et tokens de design cohérents
library app_theme_sophisticated;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../tokens/color_tokens.dart';
import '../tokens/typography_tokens.dart';
import '../tokens/spacing_tokens.dart';
/// Thème principal de l'application UnionFlow
class AppThemeSophisticated {
AppThemeSophisticated._();
// ═══════════════════════════════════════════════════════════════════════════
// THÈME PRINCIPAL - Configuration complète
// ═══════════════════════════════════════════════════════════════════════════
/// Thème clair principal
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// Couleurs principales
colorScheme: _lightColorScheme,
// Typographie
textTheme: _textTheme,
// Configuration de l'AppBar
appBarTheme: _appBarTheme,
// Configuration des cartes
cardTheme: _cardTheme,
// Configuration des boutons
elevatedButtonTheme: _elevatedButtonTheme,
filledButtonTheme: _filledButtonTheme,
outlinedButtonTheme: _outlinedButtonTheme,
textButtonTheme: _textButtonTheme,
// Configuration des champs de saisie
inputDecorationTheme: _inputDecorationTheme,
// Configuration de la navigation
navigationBarTheme: _navigationBarTheme,
navigationDrawerTheme: _navigationDrawerTheme,
// Configuration des dialogues
dialogTheme: _dialogTheme,
// Configuration des snackbars
snackBarTheme: _snackBarTheme,
// Configuration des puces
chipTheme: _chipTheme,
// Configuration des listes
listTileTheme: _listTileTheme,
// Configuration des onglets
tabBarTheme: _tabBarTheme,
// Configuration des dividers
dividerTheme: _dividerTheme,
// Configuration des icônes
iconTheme: _iconTheme,
// Configuration des surfaces
scaffoldBackgroundColor: ColorTokens.surface,
canvasColor: ColorTokens.surface,
// Configuration des animations
pageTransitionsTheme: _pageTransitionsTheme,
// Configuration des extensions
extensions: const [
_customColors,
_customSpacing,
],
);
}
// ═══════════════════════════════════════════════════════════════════════════
// SCHÉMA DE COULEURS
// ═══════════════════════════════════════════════════════════════════════════
static const ColorScheme _lightColorScheme = ColorScheme.light(
// Couleurs primaires
primary: ColorTokens.primary,
onPrimary: ColorTokens.onPrimary,
primaryContainer: ColorTokens.primaryContainer,
onPrimaryContainer: ColorTokens.onPrimaryContainer,
// Couleurs secondaires
secondary: ColorTokens.secondary,
onSecondary: ColorTokens.onSecondary,
secondaryContainer: ColorTokens.secondaryContainer,
onSecondaryContainer: ColorTokens.onSecondaryContainer,
// Couleurs tertiaires
tertiary: ColorTokens.tertiary,
onTertiary: ColorTokens.onTertiary,
tertiaryContainer: ColorTokens.tertiaryContainer,
onTertiaryContainer: ColorTokens.onTertiaryContainer,
// Couleurs d'erreur
error: ColorTokens.error,
onError: ColorTokens.onError,
errorContainer: ColorTokens.errorContainer,
onErrorContainer: ColorTokens.onErrorContainer,
// Couleurs de surface
surface: ColorTokens.surface,
onSurface: ColorTokens.onSurface,
surfaceContainerHighest: ColorTokens.surfaceVariant,
onSurfaceVariant: ColorTokens.onSurfaceVariant,
// Couleurs de contour
outline: ColorTokens.outline,
outlineVariant: ColorTokens.outlineVariant,
// Couleurs d'ombre
shadow: ColorTokens.shadow,
scrim: ColorTokens.shadow,
// Couleurs d'inversion
inverseSurface: ColorTokens.onSurface,
onInverseSurface: ColorTokens.surface,
inversePrimary: ColorTokens.primaryLight,
);
// ═══════════════════════════════════════════════════════════════════════════
// THÈME TYPOGRAPHIQUE
// ═══════════════════════════════════════════════════════════════════════════
static const TextTheme _textTheme = TextTheme(
// Display styles
displayLarge: TypographyTokens.displayLarge,
displayMedium: TypographyTokens.displayMedium,
displaySmall: TypographyTokens.displaySmall,
// Headline styles
headlineLarge: TypographyTokens.headlineLarge,
headlineMedium: TypographyTokens.headlineMedium,
headlineSmall: TypographyTokens.headlineSmall,
// Title styles
titleLarge: TypographyTokens.titleLarge,
titleMedium: TypographyTokens.titleMedium,
titleSmall: TypographyTokens.titleSmall,
// Label styles
labelLarge: TypographyTokens.labelLarge,
labelMedium: TypographyTokens.labelMedium,
labelSmall: TypographyTokens.labelSmall,
// Body styles
bodyLarge: TypographyTokens.bodyLarge,
bodyMedium: TypographyTokens.bodyMedium,
bodySmall: TypographyTokens.bodySmall,
);
// ═══════════════════════════════════════════════════════════════════════════
// THÈMES DE COMPOSANTS
// ═══════════════════════════════════════════════════════════════════════════
/// Configuration AppBar moderne (sans AppBar traditionnelle)
static const AppBarTheme _appBarTheme = AppBarTheme(
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: ColorTokens.onSurface,
surfaceTintColor: Colors.transparent,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
),
);
/// Configuration des cartes sophistiquées
static final CardTheme _cardTheme = CardTheme(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
surfaceTintColor: ColorTokens.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
margin: const EdgeInsets.all(SpacingTokens.cardMargin),
);
/// Configuration des boutons élevés
static final ElevatedButtonThemeData _elevatedButtonTheme = ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
backgroundColor: ColorTokens.primary,
foregroundColor: ColorTokens.onPrimary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des boutons remplis
static final FilledButtonThemeData _filledButtonTheme = FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: ColorTokens.primary,
foregroundColor: ColorTokens.onPrimary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des boutons avec contour
static final OutlinedButtonThemeData _outlinedButtonTheme = OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: ColorTokens.primary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
side: const BorderSide(
color: ColorTokens.outline,
width: 1.0,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des boutons texte
static final TextButtonThemeData _textButtonTheme = TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: ColorTokens.primary,
textStyle: TypographyTokens.buttonMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
minimumSize: const Size(
SpacingTokens.minButtonWidth,
SpacingTokens.buttonHeightMedium,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
),
);
/// Configuration des champs de saisie
static final InputDecorationTheme _inputDecorationTheme = InputDecorationTheme(
filled: true,
fillColor: ColorTokens.surfaceContainer,
labelStyle: TypographyTokens.inputLabel,
hintStyle: TypographyTokens.inputHint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.primary, width: 2.0),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
borderSide: const BorderSide(color: ColorTokens.error),
),
contentPadding: const EdgeInsets.all(SpacingTokens.formPadding),
);
/// Configuration de la barre de navigation
static final NavigationBarThemeData _navigationBarTheme = NavigationBarThemeData(
backgroundColor: ColorTokens.navigationBackground,
indicatorColor: ColorTokens.navigationIndicator,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return TypographyTokens.navigationLabelSelected;
}
return TypographyTokens.navigationLabel;
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: ColorTokens.navigationSelected);
}
return const IconThemeData(color: ColorTokens.navigationUnselected);
}),
);
/// Configuration du drawer de navigation
static final NavigationDrawerThemeData _navigationDrawerTheme = NavigationDrawerThemeData(
backgroundColor: ColorTokens.surfaceContainer,
elevation: SpacingTokens.elevationMd,
shadowColor: ColorTokens.shadow,
surfaceTintColor: ColorTokens.surfaceContainer,
indicatorColor: ColorTokens.primaryContainer,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return TypographyTokens.navigationLabelSelected;
}
return TypographyTokens.navigationLabel;
}),
);
/// Configuration des dialogues
static final DialogTheme _dialogTheme = DialogTheme(
backgroundColor: ColorTokens.surfaceContainer,
elevation: SpacingTokens.elevationLg,
shadowColor: ColorTokens.shadow,
surfaceTintColor: ColorTokens.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusXl),
),
titleTextStyle: TypographyTokens.headlineSmall,
contentTextStyle: TypographyTokens.bodyMedium,
);
/// Configuration des snackbars
static final SnackBarThemeData _snackBarTheme = SnackBarThemeData(
backgroundColor: ColorTokens.onSurface,
contentTextStyle: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.surface,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
behavior: SnackBarBehavior.floating,
);
/// Configuration des puces
static final ChipThemeData _chipTheme = ChipThemeData(
backgroundColor: ColorTokens.surfaceVariant,
selectedColor: ColorTokens.primaryContainer,
labelStyle: TypographyTokens.labelMedium,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
);
/// Configuration des éléments de liste
static const ListTileThemeData _listTileTheme = ListTileThemeData(
contentPadding: EdgeInsets.symmetric(
horizontal: SpacingTokens.xl,
vertical: SpacingTokens.md,
),
titleTextStyle: TypographyTokens.titleMedium,
subtitleTextStyle: TypographyTokens.bodyMedium,
leadingAndTrailingTextStyle: TypographyTokens.labelMedium,
minVerticalPadding: SpacingTokens.md,
);
/// Configuration des onglets
static final TabBarTheme _tabBarTheme = TabBarTheme(
labelColor: ColorTokens.primary,
unselectedLabelColor: ColorTokens.onSurfaceVariant,
labelStyle: TypographyTokens.titleSmall,
unselectedLabelStyle: TypographyTokens.titleSmall,
indicator: UnderlineTabIndicator(
borderSide: const BorderSide(
color: ColorTokens.primary,
width: 2.0,
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
);
/// Configuration des dividers
static const DividerThemeData _dividerTheme = DividerThemeData(
color: ColorTokens.outline,
thickness: 1.0,
space: SpacingTokens.md,
);
/// Configuration des icônes
static const IconThemeData _iconTheme = IconThemeData(
color: ColorTokens.onSurfaceVariant,
size: 24.0,
);
/// Configuration des transitions de page
static const PageTransitionsTheme _pageTransitionsTheme = PageTransitionsTheme(
builders: {
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
);
/// Extensions personnalisées - Couleurs
static const CustomColors _customColors = CustomColors();
/// Extensions personnalisées - Espacements
static const CustomSpacing _customSpacing = CustomSpacing();
}
/// Extension de couleurs personnalisées
class CustomColors extends ThemeExtension<CustomColors> {
const CustomColors();
@override
CustomColors copyWith() => const CustomColors();
@override
CustomColors lerp(ThemeExtension<CustomColors>? other, double t) {
return const CustomColors();
}
}
/// Extension d'espacements personnalisés
class CustomSpacing extends ThemeExtension<CustomSpacing> {
const CustomSpacing();
@override
CustomSpacing copyWith() => const CustomSpacing();
@override
CustomSpacing lerp(ThemeExtension<CustomSpacing>? other, double t) {
return const CustomSpacing();
}
}

View File

@@ -0,0 +1,197 @@
/// Design Tokens - Couleurs UnionFlow
///
/// Palette de couleurs Bleu Roi + Bleu Pétrole
/// Inspirée des tendances UI/UX 2024-2025
/// Basée sur les principes de Material Design 3
///
/// MODE JOUR: Bleu Roi (#4169E1) - Royal Blue
/// MODE NUIT: Bleu Pétrole (#2C5F6F) - Petroleum Blue
library color_tokens;
import 'package:flutter/material.dart';
/// Tokens de couleurs UnionFlow - Design System Unifié
class ColorTokens {
ColorTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS PRIMAIRES - MODE JOUR (Bleu Roi)
// ═══════════════════════════════════════════════════════════════════════════
/// Couleur primaire principale - Bleu Roi (Royal Blue)
static const Color primary = Color(0xFF4169E1); // Bleu roi
static const Color primaryLight = Color(0xFF6B8EF5); // Bleu roi clair
static const Color primaryDark = Color(0xFF2952C8); // Bleu roi sombre
static const Color primaryContainer = Color(0xFFE3ECFF); // Container bleu roi
static const Color onPrimary = Color(0xFFFFFFFF); // Texte sur primaire (blanc)
static const Color onPrimaryContainer = Color(0xFF001A41); // Texte sur container
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS PRIMAIRES - MODE NUIT (Bleu Pétrole)
// ═══════════════════════════════════════════════════════════════════════════
/// Couleur primaire mode nuit - Bleu Pétrole
static const Color primaryDarkMode = Color(0xFF2C5F6F); // Bleu pétrole
static const Color primaryLightDarkMode = Color(0xFF3D7A8C); // Bleu pétrole clair
static const Color primaryDarkDarkMode = Color(0xFF1B4D5C); // Bleu pétrole sombre
static const Color primaryContainerDarkMode = Color(0xFF1E3A44); // Container mode nuit
static const Color onPrimaryDarkMode = Color(0xFFE5E7EB); // Texte sur primaire (gris clair)
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS SECONDAIRES - Indigo Moderne
// ═══════════════════════════════════════════════════════════════════════════
static const Color secondary = Color(0xFF6366F1); // Indigo moderne
static const Color secondaryLight = Color(0xFF8B8FF6); // Indigo clair
static const Color secondaryDark = Color(0xFF4F46E5); // Indigo sombre
static const Color secondaryContainer = Color(0xFFE0E7FF); // Container indigo
static const Color onSecondary = Color(0xFFFFFFFF);
static const Color onSecondaryContainer = Color(0xFF1E1B3A);
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS TERTIAIRES - Vert Émeraude
// ═══════════════════════════════════════════════════════════════════════════
static const Color tertiary = Color(0xFF10B981); // Vert émeraude
static const Color tertiaryLight = Color(0xFF34D399); // Vert clair
static const Color tertiaryDark = Color(0xFF059669); // Vert sombre
static const Color tertiaryContainer = Color(0xFFD1FAE5); // Container vert
static const Color onTertiary = Color(0xFFFFFFFF);
static const Color onTertiaryContainer = Color(0xFF002114);
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS NEUTRES - MODE JOUR
// ═══════════════════════════════════════════════════════════════════════════
static const Color surface = Color(0xFFFFFFFF); // Surface principale (blanc)
static const Color surfaceVariant = Color(0xFFF8F9FA); // Surface variante (gris très clair)
static const Color surfaceContainer = Color(0xFFFFFFFF); // Container surface
static const Color surfaceContainerHigh = Color(0xFFF8F9FA); // Container élevé
static const Color surfaceContainerHighest = Color(0xFFE5E7EB); // Container max
static const Color background = Color(0xFFF8F9FA); // Background général
static const Color onSurface = Color(0xFF1F2937); // Texte principal (gris très foncé)
static const Color onSurfaceVariant = Color(0xFF6B7280); // Texte secondaire (gris moyen)
static const Color textSecondary = Color(0xFF6B7280); // Texte secondaire (alias)
static const Color outline = Color(0xFFD1D5DB); // Bordures
static const Color outlineVariant = Color(0xFFE5E7EB); // Bordures claires
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS NEUTRES - MODE NUIT
// ═══════════════════════════════════════════════════════════════════════════
static const Color surfaceDarkMode = Color(0xFF1E1E1E); // Surface principale (gris très sombre)
static const Color surfaceVariantDarkMode = Color(0xFF2C2C2C); // Surface variante
static const Color backgroundDarkMode = Color(0xFF121212); // Background général (noir profond)
static const Color onSurfaceDarkMode = Color(0xFFE5E7EB); // Texte principal (gris très clair)
static const Color onSurfaceVariantDarkMode = Color(0xFF9CA3AF); // Texte secondaire (gris moyen)
static const Color outlineDarkMode = Color(0xFF4B5563); // Bordures mode nuit
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS SÉMANTIQUES - États et feedback
// ═══════════════════════════════════════════════════════════════════════════
/// Couleurs de succès
static const Color success = Color(0xFF10B981); // Vert succès
static const Color successLight = Color(0xFF34D399); // Vert clair
static const Color successDark = Color(0xFF059669); // Vert sombre
static const Color successContainer = Color(0xFFECFDF5); // Container succès
static const Color onSuccess = Color(0xFFFFFFFF);
static const Color onSuccessContainer = Color(0xFF002114);
/// Couleurs d'erreur
static const Color error = Color(0xFFDC2626); // Rouge erreur
static const Color errorLight = Color(0xFFEF4444); // Rouge clair
static const Color errorDark = Color(0xFFB91C1C); // Rouge sombre
static const Color errorContainer = Color(0xFFFEF2F2); // Container erreur
static const Color onError = Color(0xFFFFFFFF);
static const Color onErrorContainer = Color(0xFF410002);
/// Couleurs d'avertissement
static const Color warning = Color(0xFFF59E0B); // Orange avertissement
static const Color warningLight = Color(0xFFFBBF24); // Orange clair
static const Color warningDark = Color(0xFFD97706); // Orange sombre
static const Color warningContainer = Color(0xFFFEF3C7); // Container avertissement
static const Color onWarning = Color(0xFFFFFFFF);
static const Color onWarningContainer = Color(0xFF2D1B00);
/// Couleurs d'information
static const Color info = Color(0xFF0EA5E9); // Bleu info
static const Color infoLight = Color(0xFF38BDF8); // Bleu clair
static const Color infoDark = Color(0xFF0284C7); // Bleu sombre
static const Color infoContainer = Color(0xFFE0F2FE); // Container info
static const Color onInfo = Color(0xFFFFFFFF);
static const Color onInfoContainer = Color(0xFF001D36);
// ═══════════════════════════════════════════════════════════════════════════
// COULEURS SPÉCIALISÉES - Interface avancée
// ═══════════════════════════════════════════════════════════════════════════
/// Couleurs de navigation - Mode Jour
static const Color navigationBackground = Color(0xFFFFFFFF);
static const Color navigationSelected = Color(0xFF4169E1); // Bleu roi
static const Color navigationUnselected = Color(0xFF6B7280);
static const Color navigationIndicator = Color(0xFF4169E1); // Bleu roi
/// Couleurs de navigation - Mode Nuit
static const Color navigationBackgroundDarkMode = Color(0xFF1E1E1E);
static const Color navigationSelectedDarkMode = Color(0xFF2C5F6F); // Bleu pétrole
static const Color navigationUnselectedDarkMode = Color(0xFF9CA3AF);
static const Color navigationIndicatorDarkMode = Color(0xFF2C5F6F); // Bleu pétrole
/// Couleurs d'élévation et ombres
static const Color shadow = Color(0x1A000000); // Ombre légère
static const Color shadowMedium = Color(0x33000000); // Ombre moyenne
static const Color shadowHigh = Color(0x4D000000); // Ombre forte
/// Couleurs de glassmorphism (tendance 2024-2025)
static const Color glassBackground = Color(0x80FFFFFF); // Fond verre
static const Color glassBorder = Color(0x33FFFFFF); // Bordure verre
static const Color glassOverlay = Color(0x0DFFFFFF); // Overlay verre
/// Couleurs de gradient - Mode Jour (Bleu Roi)
static const List<Color> primaryGradient = [
Color(0xFF4169E1), // Bleu roi
Color(0xFF6B8EF5), // Bleu roi clair
];
/// Couleurs de gradient - Mode Nuit (Bleu Pétrole)
static const List<Color> primaryGradientDarkMode = [
Color(0xFF2C5F6F), // Bleu pétrole
Color(0xFF3D7A8C), // Bleu pétrole clair
];
static const List<Color> secondaryGradient = [
Color(0xFF6366F1), // Indigo
Color(0xFF8B8FF6), // Indigo clair
];
static const List<Color> successGradient = [
Color(0xFF10B981), // Vert émeraude
Color(0xFF34D399), // Vert clair
];
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════
/// Obtient une couleur avec opacité
static Color withOpacity(Color color, double opacity) {
return color.withOpacity(opacity);
}
/// Obtient une couleur plus claire
static Color lighten(Color color, [double amount = 0.1]) {
final hsl = HSLColor.fromColor(color);
final lightness = (hsl.lightness + amount).clamp(0.0, 1.0);
return hsl.withLightness(lightness).toColor();
}
/// Obtient une couleur plus sombre
static Color darken(Color color, [double amount = 0.1]) {
final hsl = HSLColor.fromColor(color);
final lightness = (hsl.lightness - amount).clamp(0.0, 1.0);
return hsl.withLightness(lightness).toColor();
}
}

View File

@@ -0,0 +1,23 @@
/// Tokens de rayon pour le design system
/// Définit les rayons de bordure standardisés de l'application
library radius_tokens;
/// Tokens de rayon
class RadiusTokens {
RadiusTokens._();
/// Small - 4px
static const double sm = 4.0;
/// Medium - 8px
static const double md = 8.0;
/// Large - 12px
static const double lg = 12.0;
/// Extra large - 16px
static const double xl = 16.0;
/// Round - 50px
static const double round = 50.0;
}

View File

@@ -0,0 +1,150 @@
/// Tokens d'ombres pour le design system
/// Définit les ombres standardisées de l'application
library shadow_tokens;
import 'package:flutter/material.dart';
import 'color_tokens.dart';
/// Tokens d'ombres standardisés
///
/// Utilisation cohérente des ombres dans toute l'application.
/// Basé sur les principes de Material Design 3.
class ShadowTokens {
ShadowTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// OMBRES STANDARDS
// ═══════════════════════════════════════════════════════════════════════════
/// Ombre minimale - Pour éléments subtils
static final List<BoxShadow> xs = [
const BoxShadow(
color: ColorTokens.shadow,
blurRadius: 4,
offset: Offset(0, 1),
),
];
/// Ombre petite - Pour cards et boutons
static final List<BoxShadow> sm = [
const BoxShadow(
color: ColorTokens.shadow,
blurRadius: 8,
offset: Offset(0, 2),
),
];
/// Ombre moyenne - Pour cards importantes
static final List<BoxShadow> md = [
const BoxShadow(
color: ColorTokens.shadow,
blurRadius: 12,
offset: Offset(0, 4),
),
];
/// Ombre large - Pour modals et dialogs
static final List<BoxShadow> lg = [
const BoxShadow(
color: ColorTokens.shadowMedium,
blurRadius: 16,
offset: Offset(0, 6),
),
];
/// Ombre très large - Pour éléments flottants
static final List<BoxShadow> xl = [
const BoxShadow(
color: ColorTokens.shadowMedium,
blurRadius: 24,
offset: Offset(0, 8),
),
];
/// Ombre extra large - Pour éléments héroïques
static final List<BoxShadow> xxl = [
const BoxShadow(
color: ColorTokens.shadowHigh,
blurRadius: 32,
offset: Offset(0, 12),
spreadRadius: -4,
),
];
// ═══════════════════════════════════════════════════════════════════════════
// OMBRES COLORÉES
// ═══════════════════════════════════════════════════════════════════════════
/// Ombre primaire - Pour éléments avec couleur primaire
static final List<BoxShadow> primary = [
BoxShadow(
color: ColorTokens.primary.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre secondaire - Pour éléments avec couleur secondaire
static final List<BoxShadow> secondary = [
BoxShadow(
color: ColorTokens.secondary.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre success - Pour éléments de succès
static final List<BoxShadow> success = [
BoxShadow(
color: ColorTokens.success.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre error - Pour éléments d'erreur
static final List<BoxShadow> error = [
BoxShadow(
color: ColorTokens.error.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
/// Ombre warning - Pour éléments d'avertissement
static final List<BoxShadow> warning = [
BoxShadow(
color: ColorTokens.warning.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
// ═══════════════════════════════════════════════════════════════════════════
// OMBRES SPÉCIALES
// ═══════════════════════════════════════════════════════════════════════════
/// Ombre interne - Pour effets enfoncés
static final List<BoxShadow> inner = [
const BoxShadow(
color: ColorTokens.shadow,
blurRadius: 4,
offset: Offset(0, 2),
spreadRadius: -2,
),
];
/// Ombre diffuse - Pour glassmorphism
static final List<BoxShadow> diffuse = [
BoxShadow(
color: ColorTokens.shadow.withOpacity(0.05),
blurRadius: 20,
offset: const Offset(0, 4),
spreadRadius: 2,
),
];
/// Pas d'ombre
static const List<BoxShadow> none = [];
}

View File

@@ -0,0 +1,194 @@
/// Design Tokens - Espacements
///
/// Système d'espacement cohérent basé sur une grille de 4px
/// Optimisé pour la lisibilité et l'harmonie visuelle
library spacing_tokens;
/// Tokens d'espacement - Système de grille moderne
class SpacingTokens {
SpacingTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// ESPACEMENT DE BASE - Grille 4px
// ═══════════════════════════════════════════════════════════════════════════
/// Unité de base (4px) - Fondation du système
static const double baseUnit = 4.0;
/// Espacement minimal (2px) - Détails fins
static const double xs = baseUnit * 0.5; // 2px
/// Espacement très petit (4px) - Éléments adjacents
static const double sm = baseUnit * 1; // 4px
/// Espacement petit (8px) - Espacement interne léger
static const double md = baseUnit * 2; // 8px
/// Espacement moyen (12px) - Espacement standard
static const double lg = baseUnit * 3; // 12px
/// Espacement large (16px) - Séparation de composants
static const double xl = baseUnit * 4; // 16px
/// Espacement très large (20px) - Séparation importante
static const double xxl = baseUnit * 5; // 20px
/// Espacement extra large (24px) - Sections principales
static const double xxxl = baseUnit * 6; // 24px
/// Espacement massif (32px) - Séparation majeure
static const double huge = baseUnit * 8; // 32px
/// Espacement géant (48px) - Espacement héroïque
static const double giant = baseUnit * 12; // 48px
// ═══════════════════════════════════════════════════════════════════════════
// ESPACEMENTS SPÉCIALISÉS - Composants spécifiques
// ═══════════════════════════════════════════════════════════════════════════
/// Padding des conteneurs
static const double containerPaddingSmall = lg; // 12px
static const double containerPaddingMedium = xl; // 16px
static const double containerPaddingLarge = xxxl; // 24px
/// Marges des cartes
static const double cardMargin = xl; // 16px
static const double cardPadding = xl; // 16px
static const double cardPaddingLarge = xxxl; // 24px
/// Espacement des listes
static const double listItemSpacing = md; // 8px
static const double listSectionSpacing = xxxl; // 24px
/// Espacement des boutons
static const double buttonPaddingHorizontal = xl; // 16px
static const double buttonPaddingVertical = lg; // 12px
static const double buttonSpacing = md; // 8px
/// Espacement des formulaires
static const double formFieldSpacing = xl; // 16px
static const double formSectionSpacing = xxxl; // 24px
static const double formPadding = xl; // 16px
/// Espacement de navigation
static const double navigationPadding = xl; // 16px
static const double navigationItemSpacing = md; // 8px
static const double navigationSectionSpacing = xxxl; // 24px
/// Espacement des en-têtes
static const double headerPadding = xl; // 16px
static const double headerHeight = 56.0; // Hauteur standard
static const double headerElevation = 4.0; // Élévation
/// Espacement des onglets
static const double tabPadding = xl; // 16px
static const double tabHeight = 48.0; // Hauteur standard
/// Espacement des dialogues
static const double dialogPadding = xxxl; // 24px
static const double dialogMargin = xl; // 16px
/// Espacement des snackbars
static const double snackbarMargin = xl; // 16px
static const double snackbarPadding = xl; // 16px
// ═══════════════════════════════════════════════════════════════════════════
// RAYONS DE BORDURE - Système cohérent
// ═══════════════════════════════════════════════════════════════════════════
/// Rayon minimal (2px) - Détails subtils
static const double radiusXs = 2.0;
/// Rayon petit (4px) - Éléments fins
static const double radiusSm = 4.0;
/// Rayon moyen (8px) - Standard
static const double radiusMd = 8.0;
/// Rayon large (12px) - Cartes et composants
static const double radiusLg = 12.0;
/// Rayon très large (16px) - Conteneurs principaux
static const double radiusXl = 16.0;
/// Rayon extra large (20px) - Éléments héroïques
static const double radiusXxl = 20.0;
/// Rayon circulaire (999px) - Boutons ronds
static const double radiusCircular = 999.0;
// ═══════════════════════════════════════════════════════════════════════════
// ÉLÉVATIONS - Système d'ombres
// ═══════════════════════════════════════════════════════════════════════════
/// Élévation minimale
static const double elevationXs = 1.0;
/// Élévation petite
static const double elevationSm = 2.0;
/// Élévation moyenne
static const double elevationMd = 4.0;
/// Élévation large
static const double elevationLg = 8.0;
/// Élévation très large
static const double elevationXl = 12.0;
/// Élévation maximale
static const double elevationMax = 24.0;
// ═══════════════════════════════════════════════════════════════════════════
// DIMENSIONS FIXES - Composants standardisés
// ═══════════════════════════════════════════════════════════════════════════
/// Hauteurs de boutons
static const double buttonHeightSmall = 32.0;
static const double buttonHeightMedium = 40.0;
static const double buttonHeightLarge = 48.0;
/// Hauteurs d'éléments de liste
static const double listItemHeightSmall = 48.0;
static const double listItemHeightMedium = 56.0;
static const double listItemHeightLarge = 72.0;
/// Largeurs minimales
static const double minTouchTarget = 44.0; // Cible tactile minimale
static const double minButtonWidth = 64.0; // Largeur minimale bouton
/// Largeurs maximales
static const double maxContentWidth = 600.0; // Largeur max contenu
static const double maxDialogWidth = 400.0; // Largeur max dialogue
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════
/// Calcule un espacement basé sur l'unité de base
static double spacing(double multiplier) {
return baseUnit * multiplier;
}
/// Obtient un espacement responsive basé sur la largeur d'écran
static double responsiveSpacing(double screenWidth) {
if (screenWidth < 600) {
return xl; // Mobile
} else if (screenWidth < 1200) {
return xxxl; // Tablette
} else {
return huge; // Desktop
}
}
/// Obtient un padding responsive
static double responsivePadding(double screenWidth) {
if (screenWidth < 600) {
return xl; // 16px mobile
} else if (screenWidth < 1200) {
return xxxl; // 24px tablette
} else {
return huge; // 32px desktop
}
}
}

View File

@@ -0,0 +1,296 @@
/// Design Tokens - Typographie
///
/// Système typographique sophistiqué basé sur les tendances 2024-2025
/// Hiérarchie claire et lisibilité optimale pour applications professionnelles
library typography_tokens;
import 'package:flutter/material.dart';
import 'color_tokens.dart';
/// Tokens typographiques - Système de texte moderne
class TypographyTokens {
TypographyTokens._();
// ═══════════════════════════════════════════════════════════════════════════
// FAMILLES DE POLICES
// ═══════════════════════════════════════════════════════════════════════════
/// Police principale - Inter (moderne et lisible)
static const String primaryFontFamily = 'Inter';
/// Police secondaire - SF Pro Display (élégante)
static const String secondaryFontFamily = 'SF Pro Display';
/// Police monospace - JetBrains Mono (code et données)
static const String monospaceFontFamily = 'JetBrains Mono';
// ═══════════════════════════════════════════════════════════════════════════
// ÉCHELLE TYPOGRAPHIQUE - Basée sur Material Design 3
// ═══════════════════════════════════════════════════════════════════════════
/// Display - Titres principaux et héros
static const TextStyle displayLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 57.0,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
height: 1.12,
color: ColorTokens.onSurface,
);
static const TextStyle displayMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 45.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.0,
height: 1.16,
color: ColorTokens.onSurface,
);
static const TextStyle displaySmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 36.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.0,
height: 1.22,
color: ColorTokens.onSurface,
);
/// Headline - Titres de sections
static const TextStyle headlineLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 32.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.25,
color: ColorTokens.onSurface,
);
static const TextStyle headlineMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 28.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.29,
color: ColorTokens.onSurface,
);
static const TextStyle headlineSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 24.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.33,
color: ColorTokens.onSurface,
);
/// Title - Titres de composants
static const TextStyle titleLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 22.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.27,
color: ColorTokens.onSurface,
);
static const TextStyle titleMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.15,
height: 1.50,
color: ColorTokens.onSurface,
);
static const TextStyle titleSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.onSurface,
);
/// Label - Étiquettes et boutons
static const TextStyle labelLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.onSurface,
);
static const TextStyle labelMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 12.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.33,
color: ColorTokens.onSurface,
);
static const TextStyle labelSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 11.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
height: 1.45,
color: ColorTokens.onSurface,
);
/// Body - Texte de contenu
static const TextStyle bodyLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.50,
color: ColorTokens.onSurface,
);
static const TextStyle bodyMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
color: ColorTokens.onSurface,
);
static const TextStyle bodySmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 12.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
height: 1.33,
color: ColorTokens.onSurface,
);
// ═══════════════════════════════════════════════════════════════════════════
// STYLES SPÉCIALISÉS - Interface UnionFlow
// ═══════════════════════════════════════════════════════════════════════════
/// Navigation - Styles pour menu et navigation
static const TextStyle navigationLabel = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.navigationUnselected,
);
static const TextStyle navigationLabelSelected = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.navigationSelected,
);
/// Cartes et composants
static const TextStyle cardTitle = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 18.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.0,
height: 1.33,
color: ColorTokens.onSurface,
);
static const TextStyle cardSubtitle = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
height: 1.43,
color: ColorTokens.onSurfaceVariant,
);
static const TextStyle cardValue = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 24.0,
fontWeight: FontWeight.w700,
letterSpacing: 0.0,
height: 1.25,
color: ColorTokens.primary,
);
/// Boutons
static const TextStyle buttonLarge = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.25,
color: ColorTokens.onPrimary,
);
static const TextStyle buttonMedium = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
height: 1.29,
color: ColorTokens.onPrimary,
);
static const TextStyle buttonSmall = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 12.0,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
height: 1.33,
color: ColorTokens.onPrimary,
);
/// Formulaires
static const TextStyle inputLabel = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 14.0,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
height: 1.43,
color: ColorTokens.onSurfaceVariant,
);
static const TextStyle inputText = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.50,
color: ColorTokens.onSurface,
);
static const TextStyle inputHint = TextStyle(
fontFamily: primaryFontFamily,
fontSize: 16.0,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
height: 1.50,
color: ColorTokens.onSurfaceVariant,
);
// ═══════════════════════════════════════════════════════════════════════════
// MÉTHODES UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════
/// Applique une couleur à un style
static TextStyle withColor(TextStyle style, Color color) {
return style.copyWith(color: color);
}
/// Applique un poids de police
static TextStyle withWeight(TextStyle style, FontWeight weight) {
return style.copyWith(fontWeight: weight);
}
/// Applique une taille de police
static TextStyle withSize(TextStyle style, double size) {
return style.copyWith(fontSize: size);
}
}

View File

@@ -0,0 +1,57 @@
/// UnionFlow Design System - Point d'entrée unique
///
/// Ce fichier centralise tous les tokens et composants du Design System UnionFlow.
/// Importer ce fichier pour accéder à tous les éléments de design.
///
/// Palette de couleurs: Bleu Roi (#4169E1) + Bleu Pétrole (#2C5F6F)
/// Basé sur Material Design 3 et les tendances UI/UX 2024-2025
///
/// Usage:
/// ```dart
/// import 'package:unionflow_mobile_apps/shared/design_system/unionflow_design_system.dart';
///
/// // Utiliser les tokens
/// Container(
/// color: ColorTokens.primary,
/// padding: EdgeInsets.all(SpacingTokens.xl),
/// child: Text(
/// 'UnionFlow',
/// style: TypographyTokens.headlineMedium,
/// ),
/// );
/// ```
library unionflow_design_system;
// ═══════════════════════════════════════════════════════════════════════════
// TOKENS - Valeurs de design fondamentales
// ═══════════════════════════════════════════════════════════════════════════
/// Tokens de couleurs (Bleu Roi + Bleu Pétrole)
export 'tokens/color_tokens.dart';
/// Tokens de typographie (Inter, SF Pro Display, JetBrains Mono)
export 'tokens/typography_tokens.dart';
/// Tokens d'espacement (Grille 4px)
export 'tokens/spacing_tokens.dart';
/// Tokens de rayons de bordure
export 'tokens/radius_tokens.dart';
/// Tokens d'ombres standardisés
export 'tokens/shadow_tokens.dart';
// ═══════════════════════════════════════════════════════════════════════════
// THÈME - Configuration Material Design 3
// ═══════════════════════════════════════════════════════════════════════════
/// Thème sophistiqué (Light + Dark)
export 'theme/app_theme_sophisticated.dart';
// ═══════════════════════════════════════════════════════════════════════════
// COMPOSANTS - Widgets réutilisables
// ═══════════════════════════════════════════════════════════════════════════
/// Composants (boutons, cards, inputs, etc.)
export 'components/components.dart';

View File

@@ -0,0 +1,328 @@
/// Modèle pour les critères de recherche avancée des membres
/// Correspond au DTO Java MembreSearchCriteria
class MembreSearchCriteria {
/// Terme de recherche général (nom, prénom, email)
final String? query;
/// Recherche par nom exact ou partiel
final String? nom;
/// Recherche par prénom exact ou partiel
final String? prenom;
/// Recherche par email exact ou partiel
final String? email;
/// Filtre par numéro de téléphone
final String? telephone;
/// Liste des IDs d'organisations
final List<String>? organisationIds;
/// Liste des rôles à rechercher
final List<String>? roles;
/// Filtre par statut d'activité
final String? statut;
/// Date d'adhésion minimum (format ISO 8601)
final String? dateAdhesionMin;
/// Date d'adhésion maximum (format ISO 8601)
final String? dateAdhesionMax;
/// Âge minimum
final int? ageMin;
/// Âge maximum
final int? ageMax;
/// Filtre par région
final String? region;
/// Filtre par ville
final String? ville;
/// Filtre par profession
final String? profession;
/// Filtre par nationalité
final String? nationalite;
/// Filtre membres du bureau uniquement
final bool? membreBureau;
/// Filtre responsables uniquement
final bool? responsable;
/// Inclure les membres inactifs dans la recherche
final bool includeInactifs;
const MembreSearchCriteria({
this.query,
this.nom,
this.prenom,
this.email,
this.telephone,
this.organisationIds,
this.roles,
this.statut,
this.dateAdhesionMin,
this.dateAdhesionMax,
this.ageMin,
this.ageMax,
this.region,
this.ville,
this.profession,
this.nationalite,
this.membreBureau,
this.responsable,
this.includeInactifs = false,
});
/// Factory constructor pour créer depuis JSON
factory MembreSearchCriteria.fromJson(Map<String, dynamic> json) {
return MembreSearchCriteria(
query: json['query'] as String?,
nom: json['nom'] as String?,
prenom: json['prenom'] as String?,
email: json['email'] as String?,
telephone: json['telephone'] as String?,
organisationIds: (json['organisationIds'] as List<dynamic>?)?.cast<String>(),
roles: (json['roles'] as List<dynamic>?)?.cast<String>(),
statut: json['statut'] as String?,
dateAdhesionMin: json['dateAdhesionMin'] as String?,
dateAdhesionMax: json['dateAdhesionMax'] as String?,
ageMin: json['ageMin'] as int?,
ageMax: json['ageMax'] as int?,
region: json['region'] as String?,
ville: json['ville'] as String?,
profession: json['profession'] as String?,
nationalite: json['nationalite'] as String?,
membreBureau: json['membreBureau'] as bool?,
responsable: json['responsable'] as bool?,
includeInactifs: json['includeInactifs'] as bool? ?? false,
);
}
/// Convertit vers JSON
Map<String, dynamic> toJson() {
return {
'query': query,
'nom': nom,
'prenom': prenom,
'email': email,
'telephone': telephone,
'organisationIds': organisationIds,
'roles': roles,
'statut': statut,
'dateAdhesionMin': dateAdhesionMin,
'dateAdhesionMax': dateAdhesionMax,
'ageMin': ageMin,
'ageMax': ageMax,
'region': region,
'ville': ville,
'profession': profession,
'nationalite': nationalite,
'membreBureau': membreBureau,
'responsable': responsable,
'includeInactifs': includeInactifs,
};
}
/// Vérifie si au moins un critère de recherche est défini
bool get hasAnyCriteria {
return query?.isNotEmpty == true ||
nom?.isNotEmpty == true ||
prenom?.isNotEmpty == true ||
email?.isNotEmpty == true ||
telephone?.isNotEmpty == true ||
organisationIds?.isNotEmpty == true ||
roles?.isNotEmpty == true ||
statut?.isNotEmpty == true ||
dateAdhesionMin?.isNotEmpty == true ||
dateAdhesionMax?.isNotEmpty == true ||
ageMin != null ||
ageMax != null ||
region?.isNotEmpty == true ||
ville?.isNotEmpty == true ||
profession?.isNotEmpty == true ||
nationalite?.isNotEmpty == true ||
membreBureau != null ||
responsable != null;
}
/// Valide la cohérence des critères de recherche
bool get isValid {
// Validation des âges
if (ageMin != null && ageMax != null) {
if (ageMin! > ageMax!) {
return false;
}
}
// Validation des dates (si implémentée)
if (dateAdhesionMin != null && dateAdhesionMax != null) {
try {
final dateMin = DateTime.parse(dateAdhesionMin!);
final dateMax = DateTime.parse(dateAdhesionMax!);
if (dateMin.isAfter(dateMax)) {
return false;
}
} catch (e) {
return false;
}
}
return true;
}
/// Retourne une description textuelle des critères actifs
String get description {
final parts = <String>[];
if (query?.isNotEmpty == true) parts.add("Recherche: '$query'");
if (nom?.isNotEmpty == true) parts.add("Nom: '$nom'");
if (prenom?.isNotEmpty == true) parts.add("Prénom: '$prenom'");
if (email?.isNotEmpty == true) parts.add("Email: '$email'");
if (statut?.isNotEmpty == true) parts.add("Statut: $statut");
if (organisationIds?.isNotEmpty == true) {
parts.add("Organisations: ${organisationIds!.length}");
}
if (roles?.isNotEmpty == true) {
parts.add("Rôles: ${roles!.join(', ')}");
}
if (dateAdhesionMin?.isNotEmpty == true) {
parts.add("Adhésion >= $dateAdhesionMin");
}
if (dateAdhesionMax?.isNotEmpty == true) {
parts.add("Adhésion <= $dateAdhesionMax");
}
if (ageMin != null) parts.add("Âge >= $ageMin");
if (ageMax != null) parts.add("Âge <= $ageMax");
if (region?.isNotEmpty == true) parts.add("Région: '$region'");
if (ville?.isNotEmpty == true) parts.add("Ville: '$ville'");
if (profession?.isNotEmpty == true) parts.add("Profession: '$profession'");
if (nationalite?.isNotEmpty == true) parts.add("Nationalité: '$nationalite'");
if (membreBureau == true) parts.add("Membre bureau");
if (responsable == true) parts.add("Responsable");
return parts.join('');
}
/// Crée une copie avec des modifications
MembreSearchCriteria copyWith({
String? query,
String? nom,
String? prenom,
String? email,
String? telephone,
List<String>? organisationIds,
List<String>? roles,
String? statut,
String? dateAdhesionMin,
String? dateAdhesionMax,
int? ageMin,
int? ageMax,
String? region,
String? ville,
String? profession,
String? nationalite,
bool? membreBureau,
bool? responsable,
bool? includeInactifs,
}) {
return MembreSearchCriteria(
query: query ?? this.query,
nom: nom ?? this.nom,
prenom: prenom ?? this.prenom,
email: email ?? this.email,
telephone: telephone ?? this.telephone,
organisationIds: organisationIds ?? this.organisationIds,
roles: roles ?? this.roles,
statut: statut ?? this.statut,
dateAdhesionMin: dateAdhesionMin ?? this.dateAdhesionMin,
dateAdhesionMax: dateAdhesionMax ?? this.dateAdhesionMax,
ageMin: ageMin ?? this.ageMin,
ageMax: ageMax ?? this.ageMax,
region: region ?? this.region,
ville: ville ?? this.ville,
profession: profession ?? this.profession,
nationalite: nationalite ?? this.nationalite,
membreBureau: membreBureau ?? this.membreBureau,
responsable: responsable ?? this.responsable,
includeInactifs: includeInactifs ?? this.includeInactifs,
);
}
/// Critères vides
static const empty = MembreSearchCriteria();
/// Critères pour recherche rapide par nom/prénom
static MembreSearchCriteria quickSearch(String query) {
return MembreSearchCriteria(query: query);
}
/// Critères pour membres actifs uniquement
static const activeMembers = MembreSearchCriteria(
statut: 'ACTIF',
includeInactifs: false,
);
/// Critères pour membres du bureau
static const bureauMembers = MembreSearchCriteria(
membreBureau: true,
statut: 'ACTIF',
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MembreSearchCriteria &&
runtimeType == other.runtimeType &&
query == other.query &&
nom == other.nom &&
prenom == other.prenom &&
email == other.email &&
telephone == other.telephone &&
organisationIds == other.organisationIds &&
roles == other.roles &&
statut == other.statut &&
dateAdhesionMin == other.dateAdhesionMin &&
dateAdhesionMax == other.dateAdhesionMax &&
ageMin == other.ageMin &&
ageMax == other.ageMax &&
region == other.region &&
ville == other.ville &&
profession == other.profession &&
nationalite == other.nationalite &&
membreBureau == other.membreBureau &&
responsable == other.responsable &&
includeInactifs == other.includeInactifs;
@override
int get hashCode => Object.hashAll([
query,
nom,
prenom,
email,
telephone,
organisationIds,
roles,
statut,
dateAdhesionMin,
dateAdhesionMax,
ageMin,
ageMax,
region,
ville,
profession,
nationalite,
membreBureau,
responsable,
includeInactifs,
]);
@override
String toString() => 'MembreSearchCriteria(${description.isNotEmpty ? description : 'empty'})';
}

View File

@@ -0,0 +1,269 @@
import 'membre_search_criteria.dart';
import '../../features/members/data/models/membre_complete_model.dart';
/// Modèle pour les résultats de recherche avancée des membres
/// Correspond au DTO Java MembreSearchResultDTO
class MembreSearchResult {
/// Liste des membres trouvés
final List<MembreCompletModel> membres;
/// Nombre total de résultats (toutes pages confondues)
final int totalElements;
/// Nombre total de pages
final int totalPages;
/// Numéro de la page actuelle (0-based)
final int currentPage;
/// Taille de la page
final int pageSize;
/// Nombre d'éléments sur la page actuelle
final int numberOfElements;
/// Indique s'il y a une page suivante
final bool hasNext;
/// Indique s'il y a une page précédente
final bool hasPrevious;
/// Indique si c'est la première page
final bool isFirst;
/// Indique si c'est la dernière page
final bool isLast;
/// Critères de recherche utilisés
final MembreSearchCriteria criteria;
/// Temps d'exécution de la recherche en millisecondes
final int executionTimeMs;
/// Statistiques de recherche
final SearchStatistics? statistics;
const MembreSearchResult({
required this.membres,
required this.totalElements,
required this.totalPages,
required this.currentPage,
required this.pageSize,
required this.numberOfElements,
required this.hasNext,
required this.hasPrevious,
required this.isFirst,
required this.isLast,
required this.criteria,
required this.executionTimeMs,
this.statistics,
});
/// Factory constructor pour créer depuis JSON
factory MembreSearchResult.fromJson(Map<String, dynamic> json) {
return MembreSearchResult(
membres: (json['membres'] as List<dynamic>?)
?.map((e) => MembreCompletModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
totalElements: json['totalElements'] as int? ?? 0,
totalPages: json['totalPages'] as int? ?? 0,
currentPage: json['currentPage'] as int? ?? 0,
pageSize: json['pageSize'] as int? ?? 20,
numberOfElements: json['numberOfElements'] as int? ?? 0,
hasNext: json['hasNext'] as bool? ?? false,
hasPrevious: json['hasPrevious'] as bool? ?? false,
isFirst: json['isFirst'] as bool? ?? true,
isLast: json['isLast'] as bool? ?? true,
criteria: MembreSearchCriteria.fromJson(json['criteria'] as Map<String, dynamic>? ?? {}),
executionTimeMs: json['executionTimeMs'] as int? ?? 0,
statistics: json['statistics'] != null
? SearchStatistics.fromJson(json['statistics'] as Map<String, dynamic>)
: null,
);
}
/// Convertit vers JSON
Map<String, dynamic> toJson() {
return {
'membres': membres.map((e) => e.toJson()).toList(),
'totalElements': totalElements,
'totalPages': totalPages,
'currentPage': currentPage,
'pageSize': pageSize,
'numberOfElements': numberOfElements,
'hasNext': hasNext,
'hasPrevious': hasPrevious,
'isFirst': isFirst,
'isLast': isLast,
'criteria': criteria.toJson(),
'executionTimeMs': executionTimeMs,
'statistics': statistics?.toJson(),
};
}
/// Vérifie si les résultats sont vides
bool get isEmpty => membres.isEmpty;
/// Vérifie si les résultats ne sont pas vides
bool get isNotEmpty => membres.isNotEmpty;
/// Retourne le numéro de la page suivante (1-based pour affichage)
int get nextPageNumber => hasNext ? currentPage + 2 : -1;
/// Retourne le numéro de la page précédente (1-based pour affichage)
int get previousPageNumber => hasPrevious ? currentPage : -1;
/// Retourne une description textuelle des résultats
String get resultDescription {
if (isEmpty) {
return 'Aucun membre trouvé';
}
if (totalElements == 1) {
return '1 membre trouvé';
}
if (totalPages == 1) {
return '$totalElements membres trouvés';
}
final startElement = currentPage * pageSize + 1;
final endElement = (startElement + numberOfElements - 1).clamp(1, totalElements);
return 'Membres $startElement-$endElement sur $totalElements (page ${currentPage + 1}/$totalPages)';
}
/// Résultat vide
static MembreSearchResult empty(MembreSearchCriteria criteria) {
return MembreSearchResult(
membres: const [],
totalElements: 0,
totalPages: 0,
currentPage: 0,
pageSize: 20,
numberOfElements: 0,
hasNext: false,
hasPrevious: false,
isFirst: true,
isLast: true,
criteria: criteria,
executionTimeMs: 0,
);
}
@override
String toString() => 'MembreSearchResult($resultDescription, ${executionTimeMs}ms)';
}
/// Statistiques sur les résultats de recherche
class SearchStatistics {
/// Nombre de membres actifs dans les résultats
final int membresActifs;
/// Nombre de membres inactifs dans les résultats
final int membresInactifs;
/// Âge moyen des membres trouvés
final double ageMoyen;
/// Âge minimum des membres trouvés
final int ageMin;
/// Âge maximum des membres trouvés
final int ageMax;
/// Nombre d'organisations représentées
final int nombreOrganisations;
/// Nombre de régions représentées
final int nombreRegions;
/// Ancienneté moyenne en années
final double ancienneteMoyenne;
const SearchStatistics({
required this.membresActifs,
required this.membresInactifs,
required this.ageMoyen,
required this.ageMin,
required this.ageMax,
required this.nombreOrganisations,
required this.nombreRegions,
required this.ancienneteMoyenne,
});
/// Factory constructor pour créer depuis JSON
factory SearchStatistics.fromJson(Map<String, dynamic> json) {
return SearchStatistics(
membresActifs: json['membresActifs'] as int? ?? 0,
membresInactifs: json['membresInactifs'] as int? ?? 0,
ageMoyen: (json['ageMoyen'] as num?)?.toDouble() ?? 0.0,
ageMin: json['ageMin'] as int? ?? 0,
ageMax: json['ageMax'] as int? ?? 0,
nombreOrganisations: json['nombreOrganisations'] as int? ?? 0,
nombreRegions: json['nombreRegions'] as int? ?? 0,
ancienneteMoyenne: (json['ancienneteMoyenne'] as num?)?.toDouble() ?? 0.0,
);
}
/// Convertit vers JSON
Map<String, dynamic> toJson() {
return {
'membresActifs': membresActifs,
'membresInactifs': membresInactifs,
'ageMoyen': ageMoyen,
'ageMin': ageMin,
'ageMax': ageMax,
'nombreOrganisations': nombreOrganisations,
'nombreRegions': nombreRegions,
'ancienneteMoyenne': ancienneteMoyenne,
};
}
/// Nombre total de membres
int get totalMembres => membresActifs + membresInactifs;
/// Pourcentage de membres actifs
double get pourcentageActifs {
if (totalMembres == 0) return 0.0;
return (membresActifs / totalMembres) * 100;
}
/// Pourcentage de membres inactifs
double get pourcentageInactifs {
if (totalMembres == 0) return 0.0;
return (membresInactifs / totalMembres) * 100;
}
/// Tranche d'âge
String get trancheAge {
if (ageMin == ageMax) return '$ageMin ans';
return '$ageMin-$ageMax ans';
}
/// Description textuelle des statistiques
String get description {
final parts = <String>[];
if (totalMembres > 0) {
parts.add('$totalMembres membres');
if (membresActifs > 0) {
parts.add('${pourcentageActifs.toStringAsFixed(1)}% actifs');
}
if (ageMoyen > 0) {
parts.add('âge moyen: ${ageMoyen.toStringAsFixed(1)} ans');
}
if (nombreOrganisations > 0) {
parts.add('$nombreOrganisations organisations');
}
if (ancienneteMoyenne > 0) {
parts.add('ancienneté: ${ancienneteMoyenne.toStringAsFixed(1)} ans');
}
}
return parts.join('');
}
@override
String toString() => 'SearchStatistics($description)';
}

View File

@@ -0,0 +1,396 @@
/// Widget adaptatif révolutionnaire avec morphing intelligent
/// Transformation dynamique selon le rôle utilisateur avec animations fluides
library adaptive_widget;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../features/authentication/data/models/user.dart';
import '../../features/authentication/data/models/user_role.dart';
import '../../features/authentication/data/datasources/permission_engine.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
/// Widget adaptatif révolutionnaire qui se transforme selon le rôle utilisateur
///
/// Fonctionnalités :
/// - Morphing intelligent avec animations fluides
/// - Widgets spécifiques par rôle
/// - Vérification de permissions intégrée
/// - Fallback gracieux pour les rôles non supportés
/// - Cache des widgets pour les performances
class AdaptiveWidget extends StatefulWidget {
/// Widgets spécifiques par rôle utilisateur
final Map<UserRole, Widget Function()> roleWidgets;
/// Permissions requises pour afficher le widget
final List<String> requiredPermissions;
/// Widget affiché si les permissions sont insuffisantes
final Widget? fallbackWidget;
/// Widget affiché pendant le chargement
final Widget? loadingWidget;
/// Activer les animations de morphing
final bool enableMorphing;
/// Durée de l'animation de morphing
final Duration morphingDuration;
/// Courbe d'animation
final Curve animationCurve;
/// Contexte organisationnel pour les permissions
final String? organizationId;
/// Activer l'audit trail
final bool auditLog;
/// Constructeur du widget adaptatif
const AdaptiveWidget({
super.key,
required this.roleWidgets,
this.requiredPermissions = const [],
this.fallbackWidget,
this.loadingWidget,
this.enableMorphing = true,
this.morphingDuration = const Duration(milliseconds: 800),
this.animationCurve = Curves.easeInOutCubic,
this.organizationId,
this.auditLog = true,
});
@override
State<AdaptiveWidget> createState() => _AdaptiveWidgetState();
}
class _AdaptiveWidgetState extends State<AdaptiveWidget>
with TickerProviderStateMixin {
/// Cache des widgets construits pour éviter les reconstructions
final Map<UserRole, Widget> _widgetCache = {};
/// Contrôleur d'animation pour le morphing
late AnimationController _morphController;
/// Animation d'opacité
late Animation<double> _opacityAnimation;
/// Animation d'échelle
late Animation<double> _scaleAnimation;
/// Rôle utilisateur précédent pour détecter les changements
UserRole? _previousRole;
@override
void initState() {
super.initState();
_initializeAnimations();
}
@override
void dispose() {
_morphController.dispose();
super.dispose();
}
/// Initialise les animations de morphing
void _initializeAnimations() {
_morphController = AnimationController(
duration: widget.morphingDuration,
vsync: this,
);
_opacityAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _morphController,
curve: widget.animationCurve,
));
_scaleAnimation = Tween<double>(
begin: 0.95,
end: 1.0,
).animate(CurvedAnimation(
parent: _morphController,
curve: widget.animationCurve,
));
// Démarrer l'animation initiale
_morphController.forward();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
// État de chargement
if (state is AuthLoading) {
return widget.loadingWidget ?? _buildLoadingWidget();
}
// État non authentifié
if (state is! AuthAuthenticated) {
return _buildForRole(UserRole.visitor);
}
final user = state.user;
final currentRole = user.primaryRole;
// Détecter le changement de rôle pour déclencher l'animation
if (_previousRole != null && _previousRole != currentRole && widget.enableMorphing) {
_triggerMorphing();
}
_previousRole = currentRole;
return FutureBuilder<bool>(
future: _checkPermissions(user),
builder: (context, permissionSnapshot) {
if (permissionSnapshot.connectionState == ConnectionState.waiting) {
return widget.loadingWidget ?? _buildLoadingWidget();
}
final hasPermissions = permissionSnapshot.data ?? false;
if (!hasPermissions) {
return widget.fallbackWidget ?? _buildUnauthorizedWidget();
}
return _buildForRole(currentRole);
},
);
},
);
}
/// Construit le widget pour un rôle spécifique
Widget _buildForRole(UserRole role) {
// Vérifier le cache
if (_widgetCache.containsKey(role)) {
return _wrapWithAnimation(_widgetCache[role]!);
}
// Trouver le widget approprié
Widget? widget = _findWidgetForRole(role);
widget ??= this.widget.fallbackWidget ?? _buildUnsupportedRoleWidget(role);
// Mettre en cache
_widgetCache[role] = widget;
return _wrapWithAnimation(widget);
}
/// Trouve le widget approprié pour un rôle
Widget? _findWidgetForRole(UserRole role) {
// Vérification directe
if (widget.roleWidgets.containsKey(role)) {
return widget.roleWidgets[role]!();
}
// Recherche du meilleur match par niveau de rôle
UserRole? bestMatch;
for (final availableRole in widget.roleWidgets.keys) {
if (availableRole.level <= role.level) {
if (bestMatch == null || availableRole.level > bestMatch.level) {
bestMatch = availableRole;
}
}
}
return bestMatch != null ? widget.roleWidgets[bestMatch]!() : null;
}
/// Enveloppe le widget avec les animations
Widget _wrapWithAnimation(Widget child) {
if (!widget.enableMorphing) return child;
return AnimatedBuilder(
animation: _morphController,
builder: (context, _) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: child,
),
);
},
);
}
/// Déclenche l'animation de morphing
void _triggerMorphing() {
_morphController.reset();
_morphController.forward();
// Vider le cache pour forcer la reconstruction
_widgetCache.clear();
}
/// Vérifie les permissions requises
Future<bool> _checkPermissions(User user) async {
if (widget.requiredPermissions.isEmpty) return true;
final results = await PermissionEngine.hasPermissions(
user,
widget.requiredPermissions,
organizationId: widget.organizationId,
auditLog: widget.auditLog,
);
return results.values.every((hasPermission) => hasPermission);
}
/// Widget de chargement par défaut
Widget _buildLoadingWidget() {
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
/// Widget non autorisé par défaut
Widget _buildUnauthorizedWidget() {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock_outline,
size: 48,
color: Theme.of(context).disabledColor,
),
const SizedBox(height: 8),
Text(
'Accès non autorisé',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).disabledColor,
),
),
const SizedBox(height: 4),
Text(
'Vous n\'avez pas les permissions nécessaires',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).disabledColor,
),
textAlign: TextAlign.center,
),
],
),
);
}
/// Widget pour rôle non supporté
Widget _buildUnsupportedRoleWidget(UserRole role) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.warning_outlined,
size: 48,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 8),
Text(
'Rôle non supporté',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
const SizedBox(height: 4),
Text(
'Le rôle ${role.displayName} n\'est pas supporté par ce widget',
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
],
),
);
}
}
/// Widget sécurisé avec vérification de permissions intégrée
///
/// Version simplifiée d'AdaptiveWidget pour les cas où seules
/// les permissions importent, pas le rôle spécifique
class SecureWidget extends StatelessWidget {
/// Permissions requises pour afficher le widget
final List<String> requiredPermissions;
/// Widget à afficher si autorisé
final Widget child;
/// Widget à afficher si non autorisé
final Widget? unauthorizedWidget;
/// Widget à afficher pendant le chargement
final Widget? loadingWidget;
/// Contexte organisationnel
final String? organizationId;
/// Activer l'audit trail
final bool auditLog;
/// Constructeur du widget sécurisé
const SecureWidget({
super.key,
required this.requiredPermissions,
required this.child,
this.unauthorizedWidget,
this.loadingWidget,
this.organizationId,
this.auditLog = true,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthLoading) {
return loadingWidget ?? const SizedBox.shrink();
}
if (state is! AuthAuthenticated) {
return unauthorizedWidget ?? const SizedBox.shrink();
}
return FutureBuilder<bool>(
future: _checkPermissions(state.user),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return loadingWidget ?? const SizedBox.shrink();
}
final hasPermissions = snapshot.data ?? false;
if (!hasPermissions) {
return unauthorizedWidget ?? const SizedBox.shrink();
}
return child;
},
);
},
);
}
/// Vérifie les permissions requises
Future<bool> _checkPermissions(User user) async {
if (requiredPermissions.isEmpty) return true;
final results = await PermissionEngine.hasPermissions(
user,
requiredPermissions,
organizationId: organizationId,
auditLog: auditLog,
);
return results.values.every((hasPermission) => hasPermission);
}
}

View File

@@ -0,0 +1,292 @@
/// Dialogue de confirmation réutilisable
/// Utilisé pour confirmer les actions critiques (suppression, etc.)
library confirmation_dialog;
import 'package:flutter/material.dart';
/// Type d'action pour personnaliser l'apparence du dialogue
enum ConfirmationAction {
delete,
deactivate,
activate,
cancel,
warning,
info,
}
/// Dialogue de confirmation générique
class ConfirmationDialog extends StatelessWidget {
final String title;
final String message;
final String confirmText;
final String cancelText;
final ConfirmationAction action;
final VoidCallback? onConfirm;
final VoidCallback? onCancel;
const ConfirmationDialog({
super.key,
required this.title,
required this.message,
this.confirmText = 'Confirmer',
this.cancelText = 'Annuler',
this.action = ConfirmationAction.warning,
this.onConfirm,
this.onCancel,
});
/// Constructeur pour suppression
const ConfirmationDialog.delete({
super.key,
required this.title,
required this.message,
this.confirmText = 'Supprimer',
this.cancelText = 'Annuler',
this.onConfirm,
this.onCancel,
}) : action = ConfirmationAction.delete;
/// Constructeur pour désactivation
const ConfirmationDialog.deactivate({
super.key,
required this.title,
required this.message,
this.confirmText = 'Désactiver',
this.cancelText = 'Annuler',
this.onConfirm,
this.onCancel,
}) : action = ConfirmationAction.deactivate;
/// Constructeur pour activation
const ConfirmationDialog.activate({
super.key,
required this.title,
required this.message,
this.confirmText = 'Activer',
this.cancelText = 'Annuler',
this.onConfirm,
this.onCancel,
}) : action = ConfirmationAction.activate;
@override
Widget build(BuildContext context) {
final colors = _getColors();
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Row(
children: [
Icon(
_getIcon(),
color: colors['icon'],
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: TextStyle(
color: colors['title'],
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
],
),
content: Text(
message,
style: const TextStyle(
fontSize: 16,
height: 1.5,
),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
onCancel?.call();
},
child: Text(
cancelText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
onConfirm?.call();
},
style: ElevatedButton.styleFrom(
backgroundColor: colors['button'],
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
confirmText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
IconData _getIcon() {
switch (action) {
case ConfirmationAction.delete:
return Icons.delete_forever;
case ConfirmationAction.deactivate:
return Icons.block;
case ConfirmationAction.activate:
return Icons.check_circle;
case ConfirmationAction.cancel:
return Icons.cancel;
case ConfirmationAction.warning:
return Icons.warning;
case ConfirmationAction.info:
return Icons.info;
}
}
Map<String, Color> _getColors() {
switch (action) {
case ConfirmationAction.delete:
return {
'icon': Colors.red,
'title': Colors.red[700]!,
'button': Colors.red,
};
case ConfirmationAction.deactivate:
return {
'icon': Colors.orange,
'title': Colors.orange[700]!,
'button': Colors.orange,
};
case ConfirmationAction.activate:
return {
'icon': Colors.green,
'title': Colors.green[700]!,
'button': Colors.green,
};
case ConfirmationAction.cancel:
return {
'icon': Colors.grey,
'title': Colors.grey[700]!,
'button': Colors.grey,
};
case ConfirmationAction.warning:
return {
'icon': Colors.amber,
'title': Colors.amber[700]!,
'button': Colors.amber,
};
case ConfirmationAction.info:
return {
'icon': Colors.blue,
'title': Colors.blue[700]!,
'button': Colors.blue,
};
}
}
}
/// Fonction utilitaire pour afficher un dialogue de confirmation
Future<bool> showConfirmationDialog({
required BuildContext context,
required String title,
required String message,
String confirmText = 'Confirmer',
String cancelText = 'Annuler',
ConfirmationAction action = ConfirmationAction.warning,
}) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog(
title: title,
message: message,
confirmText: confirmText,
cancelText: cancelText,
action: action,
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}
/// Fonction utilitaire pour dialogue de suppression
Future<bool> showDeleteConfirmation({
required BuildContext context,
required String itemName,
String? additionalMessage,
}) async {
final message = additionalMessage != null
? 'Êtes-vous sûr de vouloir supprimer "$itemName" ?\n\n$additionalMessage\n\nCette action est irréversible.'
: 'Êtes-vous sûr de vouloir supprimer "$itemName" ?\n\nCette action est irréversible.';
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog.delete(
title: 'Confirmer la suppression',
message: message,
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}
/// Fonction utilitaire pour dialogue de désactivation
Future<bool> showDeactivateConfirmation({
required BuildContext context,
required String itemName,
String? reason,
}) async {
final message = reason != null
? 'Êtes-vous sûr de vouloir désactiver "$itemName" ?\n\n$reason'
: 'Êtes-vous sûr de vouloir désactiver "$itemName" ?';
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog.deactivate(
title: 'Confirmer la désactivation',
message: message,
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}
/// Fonction utilitaire pour dialogue d'activation
Future<bool> showActivateConfirmation({
required BuildContext context,
required String itemName,
}) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => ConfirmationDialog.activate(
title: 'Confirmer l\'activation',
message: 'Êtes-vous sûr de vouloir activer "$itemName" ?',
onConfirm: () {},
onCancel: () {},
),
);
return result ?? false;
}

View File

@@ -0,0 +1,168 @@
/// Widget d'erreur réutilisable pour toute l'application
library error_widget;
import 'package:flutter/material.dart';
/// Widget d'erreur avec message et bouton de retry
class AppErrorWidget extends StatelessWidget {
/// Message d'erreur à afficher
final String message;
/// Callback appelé lors du clic sur le bouton retry
final VoidCallback? onRetry;
/// Icône personnalisée (optionnel)
final IconData? icon;
/// Titre personnalisé (optionnel)
final String? title;
const AppErrorWidget({
super.key,
required this.message,
this.onRetry,
this.icon,
this.title,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon ?? Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
title ?? 'Oups !',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (onRetry != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
],
),
),
);
}
}
/// Widget d'erreur réseau spécifique
class NetworkErrorWidget extends StatelessWidget {
final VoidCallback? onRetry;
const NetworkErrorWidget({
super.key,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return AppErrorWidget(
message: 'Impossible de se connecter au serveur.\nVérifiez votre connexion internet.',
onRetry: onRetry,
icon: Icons.wifi_off,
title: 'Pas de connexion',
);
}
}
/// Widget d'erreur de permissions
class PermissionErrorWidget extends StatelessWidget {
final String? message;
const PermissionErrorWidget({
super.key,
this.message,
});
@override
Widget build(BuildContext context) {
return AppErrorWidget(
message: message ?? 'Vous n\'avez pas les permissions nécessaires pour accéder à cette ressource.',
icon: Icons.lock_outline,
title: 'Accès refusé',
);
}
}
/// Widget d'erreur "Aucune donnée"
class EmptyDataWidget extends StatelessWidget {
final String message;
final IconData? icon;
final VoidCallback? onAction;
final String? actionLabel;
const EmptyDataWidget({
super.key,
required this.message,
this.icon,
this.onAction,
this.actionLabel,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon ?? Icons.inbox_outlined,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (onAction != null && actionLabel != null) ...[
const SizedBox(height: 24),
ElevatedButton(
onPressed: onAction,
child: Text(actionLabel!),
),
],
],
),
),
);
}
}

View File

@@ -0,0 +1,244 @@
/// Widgets de chargement réutilisables pour toute l'application
library loading_widget;
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
/// Widget de chargement simple avec CircularProgressIndicator
class AppLoadingWidget extends StatelessWidget {
final String? message;
final double? size;
const AppLoadingWidget({
super.key,
this.message,
this.size,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size ?? 40,
height: size ?? 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
),
if (message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
],
),
);
}
}
/// Widget de chargement avec effet shimmer pour les listes
class ShimmerListLoading extends StatelessWidget {
final int itemCount;
final double itemHeight;
const ShimmerListLoading({
super.key,
this.itemCount = 5,
this.itemHeight = 80,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: itemCount,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: itemHeight,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
),
),
);
},
);
}
}
/// Widget de chargement avec effet shimmer pour les cartes
class ShimmerCardLoading extends StatelessWidget {
final double height;
final double? width;
const ShimmerCardLoading({
super.key,
this.height = 120,
this.width,
});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
);
}
}
/// Widget de chargement avec effet shimmer pour une grille
class ShimmerGridLoading extends StatelessWidget {
final int itemCount;
final int crossAxisCount;
final double childAspectRatio;
const ShimmerGridLoading({
super.key,
this.itemCount = 6,
this.crossAxisCount = 2,
this.childAspectRatio = 1.0,
});
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: childAspectRatio,
),
itemCount: itemCount,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
);
},
);
}
}
/// Widget de chargement pour les détails d'un élément
class ShimmerDetailLoading extends StatelessWidget {
const ShimmerDetailLoading({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
const SizedBox(height: 16),
// Title
Container(
height: 24,
width: double.infinity,
color: Colors.white,
),
const SizedBox(height: 8),
// Subtitle
Container(
height: 16,
width: 200,
color: Colors.white,
),
const SizedBox(height: 24),
// Content lines
...List.generate(5, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
height: 12,
width: double.infinity,
color: Colors.white,
),
);
}),
],
),
),
);
}
}
/// Widget de chargement inline (petit)
class InlineLoadingWidget extends StatelessWidget {
final String? message;
const InlineLoadingWidget({
super.key,
this.message,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
),
if (message != null) ...[
const SizedBox(width: 8),
Text(
message!,
style: Theme.of(context).textTheme.bodySmall,
),
],
],
);
}
}