Refactoring - Version OK
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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'})';
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
396
unionflow-mobile-apps/lib/shared/widgets/adaptive_widget.dart
Normal file
396
unionflow-mobile-apps/lib/shared/widgets/adaptive_widget.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
168
unionflow-mobile-apps/lib/shared/widgets/error_widget.dart
Normal file
168
unionflow-mobile-apps/lib/shared/widgets/error_widget.dart
Normal 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!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
244
unionflow-mobile-apps/lib/shared/widgets/loading_widget.dart
Normal file
244
unionflow-mobile-apps/lib/shared/widgets/loading_widget.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user