feat(unionflow): ajout Spec-Kit, constitution, mission mutuelles

- Config Spec-Kit pour Spec-Driven Development
- CONSTITUTION.md + .specify/memory/constitution.md
- Commandes Cursor /speckit.*, règles projet
- Mission: associations + mutuelles d'épargne et de financement
- .gitignore: versionner config spec-kit unionflow

Made-with: Cursor
This commit is contained in:
dahoud
2026-02-27 14:41:07 +00:00
parent 144b68f8e7
commit b1957c1c81
631 changed files with 104070 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
/// UnionFlow Primary Button - Bouton principal
///
/// Bouton primaire avec la couleur Bleu Roi (#4169E1)
/// Utilisé pour les actions principales (connexion, enregistrer, valider, etc.)
library uf_primary_button;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Bouton primaire UnionFlow
///
/// Usage:
/// ```dart
/// UFPrimaryButton(
/// label: 'Connexion',
/// onPressed: () => login(),
/// icon: Icons.login,
/// isLoading: false,
/// )
/// ```
class UFPrimaryButton extends StatelessWidget {
/// Texte du bouton
final String label;
/// Callback appelé lors du clic
final VoidCallback? onPressed;
/// Indique si le bouton est en chargement
final bool isLoading;
/// Icône optionnelle à gauche du texte
final IconData? icon;
/// Bouton pleine largeur
final bool isFullWidth;
/// Hauteur personnalisée (optionnel)
final double? height;
const UFPrimaryButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.icon,
this.isFullWidth = false,
this.height,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: isFullWidth ? double.infinity : null,
height: height ?? SpacingTokens.buttonHeightLarge,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: ColorTokens.primary, // Bleu roi
foregroundColor: ColorTokens.onPrimary, // Blanc
disabledBackgroundColor: ColorTokens.primary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onPrimary.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
ColorTokens.onPrimary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: TypographyTokens.buttonLarge,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,82 @@
/// UnionFlow Secondary Button - Bouton secondaire
///
/// Bouton secondaire avec la couleur Indigo (#6366F1)
/// Utilisé pour les actions secondaires (annuler, retour, etc.)
library uf_secondary_button;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Bouton secondaire UnionFlow
class UFSecondaryButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
final bool isFullWidth;
final double? height;
const UFSecondaryButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.icon,
this.isFullWidth = false,
this.height,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: isFullWidth ? double.infinity : null,
height: height ?? SpacingTokens.buttonHeightLarge,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: ColorTokens.secondary, // Indigo
foregroundColor: ColorTokens.onSecondary, // Blanc
disabledBackgroundColor: ColorTokens.secondary.withOpacity(0.5),
disabledForegroundColor: ColorTokens.onSecondary.withOpacity(0.7),
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.buttonPaddingHorizontal,
vertical: SpacingTokens.buttonPaddingVertical,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
ColorTokens.onSecondary,
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: SpacingTokens.md),
],
Text(
label,
style: TypographyTokens.buttonLarge,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import '../../unionflow_design_system.dart';
/// Card standardisé UnionFlow
///
/// Composant Card unifié avec 3 styles prédéfinis :
/// - elevated : Card avec ombre (par défaut)
/// - outlined : Card avec bordure
/// - filled : Card avec fond coloré
///
/// Usage:
/// ```dart
/// UFCard(
/// child: Text('Contenu'),
/// )
///
/// UFCard.outlined(
/// child: Text('Contenu'),
/// )
///
/// UFCard.filled(
/// color: ColorTokens.primary,
/// child: Text('Contenu'),
/// )
/// ```
class UFCard extends StatelessWidget {
final Widget child;
final EdgeInsets? padding;
final EdgeInsets? margin;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final UFCardStyle style;
final Color? color;
final Color? borderColor;
final double? borderWidth;
final double? elevation;
final double? borderRadius;
/// Card avec ombre (style par défaut)
const UFCard({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
this.color,
this.elevation,
this.borderRadius,
}) : style = UFCardStyle.elevated,
borderColor = null,
borderWidth = null;
/// Card avec bordure
const UFCard.outlined({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
this.color,
this.borderColor,
this.borderWidth,
this.borderRadius,
}) : style = UFCardStyle.outlined,
elevation = null;
/// Card avec fond coloré
const UFCard.filled({
super.key,
required this.child,
this.padding,
this.margin,
this.onTap,
this.onLongPress,
required this.color,
this.borderRadius,
}) : style = UFCardStyle.filled,
borderColor = null,
borderWidth = null,
elevation = null;
@override
Widget build(BuildContext context) {
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.cardPadding);
final effectiveMargin = margin ?? EdgeInsets.zero;
final effectiveBorderRadius = borderRadius ?? SpacingTokens.radiusLg;
Widget content = Container(
padding: effectivePadding,
decoration: _getDecoration(effectiveBorderRadius),
child: child,
);
if (onTap != null || onLongPress != null) {
content = InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(effectiveBorderRadius),
child: content,
);
}
return Container(
margin: effectiveMargin,
child: content,
);
}
BoxDecoration _getDecoration(double radius) {
switch (style) {
case UFCardStyle.elevated:
return BoxDecoration(
color: color ?? ColorTokens.surface,
borderRadius: BorderRadius.circular(radius),
boxShadow: elevation != null
? [
BoxShadow(
color: ColorTokens.shadow,
blurRadius: elevation!,
offset: const Offset(0, 2),
),
]
: ShadowTokens.sm,
);
case UFCardStyle.outlined:
return BoxDecoration(
color: color ?? ColorTokens.surface,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: borderColor ?? ColorTokens.outline,
width: borderWidth ?? 1.0,
),
);
case UFCardStyle.filled:
return BoxDecoration(
color: color ?? ColorTokens.surfaceContainer,
borderRadius: BorderRadius.circular(radius),
);
}
}
}
/// Styles de Card disponibles
enum UFCardStyle {
/// Card avec ombre
elevated,
/// Card avec bordure
outlined,
/// Card avec fond coloré
filled,
}

View File

@@ -0,0 +1,92 @@
/// UnionFlow Info Card - Card d'information générique
///
/// Card blanche avec titre, icône et contenu personnalisable
library uf_info_card;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
import '../../tokens/shadow_tokens.dart';
/// Card d'information générique
///
/// Usage:
/// ```dart
/// UFInfoCard(
/// title: 'État du système',
/// icon: Icons.health_and_safety,
/// iconColor: ColorTokens.primary,
/// trailing: Container(...), // Badge ou autre widget
/// child: Column(...), // Contenu de la card
/// )
/// ```
class UFInfoCard extends StatelessWidget {
/// Titre de la card
final String title;
/// Icône du titre
final IconData icon;
/// Couleur de l'icône (par défaut: primary)
final Color? iconColor;
/// Widget à droite du titre (badge, bouton, etc.)
final Widget? trailing;
/// Contenu de la card
final Widget child;
/// Padding de la card (par défaut: xl)
final EdgeInsets? padding;
const UFInfoCard({
super.key,
required this.title,
required this.icon,
this.iconColor,
this.trailing,
required this.child,
this.padding,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectivePadding = padding ?? const EdgeInsets.all(SpacingTokens.xl);
return Container(
padding: effectivePadding,
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.sm,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec titre et trailing
Row(
children: [
Icon(icon, color: effectiveIconColor, size: 20),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Text(
title,
style: TypographyTokens.titleMedium.copyWith(
color: ColorTokens.onSurface,
),
),
),
if (trailing != null) trailing!,
],
),
const SizedBox(height: SpacingTokens.xl),
// Contenu
child,
],
),
);
}
}

View File

@@ -0,0 +1,76 @@
/// UnionFlow Metric Card - Card de métrique système
///
/// Card compacte pour afficher une métrique système (CPU, RAM, etc.)
library uf_metric_card;
import 'package:flutter/material.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Card de métrique système
///
/// Usage:
/// ```dart
/// UFMetricCard(
/// label: 'CPU',
/// value: '23.5%',
/// icon: Icons.memory,
/// color: ColorTokens.success,
/// )
/// ```
class UFMetricCard extends StatelessWidget {
/// Label de la métrique (ex: "CPU")
final String label;
/// Valeur de la métrique (ex: "23.5%")
final String value;
/// Icône représentant la métrique
final IconData icon;
/// Couleur de la métrique (optionnel)
final Color? color;
const UFMetricCard({
super.key,
required this.label,
required this.value,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 16),
const SizedBox(height: SpacingTokens.sm),
Text(
value,
style: TypographyTokens.labelSmall.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
Text(
label,
style: TypographyTokens.labelSmall.copyWith(
fontSize: 9,
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -0,0 +1,143 @@
/// UnionFlow Stat Card - Card de statistiques
///
/// Card affichant une statistique avec icône, titre, valeur et sous-titre optionnel
/// Utilisé dans le dashboard pour afficher les métriques clés
library uf_stat_card;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Card de statistiques UnionFlow
///
/// Usage:
/// ```dart
/// UFStatCard(
/// title: 'Membres',
/// value: '142',
/// icon: Icons.people,
/// iconColor: ColorTokens.primary,
/// subtitle: '+5 ce mois',
/// onTap: () => navigateToMembers(),
/// )
/// ```
class UFStatCard extends StatelessWidget {
/// Titre de la statistique (ex: "Membres")
final String title;
/// Valeur de la statistique (ex: "142")
final String value;
/// Icône représentant la statistique
final IconData icon;
/// Couleur de l'icône (par défaut: primary)
final Color? iconColor;
/// Sous-titre optionnel (ex: "+5 ce mois")
final String? subtitle;
/// Callback appelé lors du clic sur la card
final VoidCallback? onTap;
/// Couleur de fond de l'icône (par défaut: iconColor avec opacité)
final Color? iconBackgroundColor;
const UFStatCard({
super.key,
required this.title,
required this.value,
required this.icon,
this.iconColor,
this.subtitle,
this.onTap,
this.iconBackgroundColor,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
final effectiveIconBgColor = iconBackgroundColor ??
effectiveIconColor.withOpacity(0.1);
return Card(
elevation: SpacingTokens.elevationSm,
shadowColor: ColorTokens.shadow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.cardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header avec icône et flèche
Row(
children: [
// Icône avec background coloré
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: effectiveIconBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 24,
),
),
const Spacer(),
// Flèche si cliquable
if (onTap != null)
const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorTokens.onSurfaceVariant,
),
],
),
const SizedBox(height: SpacingTokens.lg),
// Titre
Text(
title,
style: TypographyTokens.labelLarge.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
const SizedBox(height: SpacingTokens.sm),
// Valeur
Text(
value,
style: TypographyTokens.cardValue.copyWith(
color: ColorTokens.onSurface,
),
),
// Sous-titre optionnel
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.sm),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
],
),
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
/// UnionFlow Components - Export centralisé
///
/// Ce fichier exporte tous les composants réutilisables du Design System
library components;
// ═══════════════════════════════════════════════════════════════════════════
// BOUTONS
// ═══════════════════════════════════════════════════════════════════════════
export 'buttons/uf_primary_button.dart';
export 'buttons/uf_secondary_button.dart';
// ═══════════════════════════════════════════════════════════════════════════
// CARDS & CONTAINERS
// ═══════════════════════════════════════════════════════════════════════════
export 'cards/uf_card.dart';
export 'cards/uf_stat_card.dart';
export 'cards/uf_info_card.dart';
export 'cards/uf_metric_card.dart';
export 'uf_container.dart';
// ═══════════════════════════════════════════════════════════════════════════
// INPUTS
// ═══════════════════════════════════════════════════════════════════════════
export 'inputs/uf_switch_tile.dart';
export 'inputs/uf_dropdown_tile.dart';
// ═══════════════════════════════════════════════════════════════════════════
// HEADERS & APPBAR
// ═══════════════════════════════════════════════════════════════════════════
export 'uf_header.dart';
export 'uf_page_header.dart';
export 'uf_app_bar.dart';
// Composants supplémentaires à exporter quand créés :
// export 'buttons/uf_outline_button.dart';
// export 'buttons/uf_text_button.dart';
// export 'cards/uf_event_card.dart';
// export 'inputs/uf_text_field.dart';

View File

@@ -0,0 +1,99 @@
/// UnionFlow Dropdown Tile - Ligne de paramètre avec dropdown
///
/// Tile avec titre et dropdown pour les paramètres
library uf_dropdown_tile;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Tile de paramètre avec dropdown
///
/// Usage:
/// ```dart
/// UFDropdownTile<String>(
/// title: 'Niveau de log',
/// value: 'INFO',
/// items: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'],
/// onChanged: (value) => setState(() => _logLevel = value),
/// )
/// ```
class UFDropdownTile<T> extends StatelessWidget {
/// Titre du paramètre
final String title;
/// Valeur actuelle
final T value;
/// Liste des options
final List<T> items;
/// Callback appelé lors du changement
final ValueChanged<T?>? onChanged;
/// Couleur de fond (par défaut: surfaceVariant)
final Color? backgroundColor;
/// Fonction pour afficher le texte d'un item (par défaut: toString())
final String Function(T)? itemBuilder;
const UFDropdownTile({
super.key,
required this.title,
required this.value,
required this.items,
this.onChanged,
this.backgroundColor,
this.itemBuilder,
});
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? ColorTokens.surfaceVariant;
final effectiveItemBuilder = itemBuilder ?? (item) => item.toString();
return Container(
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: effectiveBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Row(
children: [
Expanded(
child: Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.lg),
decoration: BoxDecoration(
color: ColorTokens.surface,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(color: ColorTokens.outline),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
value: value,
onChanged: onChanged,
items: items.map((item) {
return DropdownMenuItem<T>(
value: item,
child: Text(effectiveItemBuilder(item)),
);
}).toList(),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,90 @@
/// UnionFlow Switch Tile - Ligne de paramètre avec switch
///
/// Tile avec titre, description et switch pour les paramètres
library uf_switch_tile;
import 'package:flutter/material.dart';
import '../../tokens/color_tokens.dart';
import '../../tokens/spacing_tokens.dart';
import '../../tokens/typography_tokens.dart';
/// Tile de paramètre avec switch
///
/// Usage:
/// ```dart
/// UFSwitchTile(
/// title: 'Notifications',
/// subtitle: 'Activer les notifications push',
/// value: true,
/// onChanged: (value) => setState(() => _notifications = value),
/// )
/// ```
class UFSwitchTile extends StatelessWidget {
/// Titre du paramètre
final String title;
/// Description du paramètre
final String subtitle;
/// Valeur actuelle du switch
final bool value;
/// Callback appelé lors du changement
final ValueChanged<bool>? onChanged;
/// Couleur de fond (par défaut: surfaceVariant)
final Color? backgroundColor;
const UFSwitchTile({
super.key,
required this.title,
required this.subtitle,
required this.value,
this.onChanged,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
final effectiveBgColor = backgroundColor ?? ColorTokens.surfaceVariant;
return Container(
margin: const EdgeInsets.only(bottom: SpacingTokens.lg),
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: effectiveBgColor,
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.onSurface,
),
),
Text(
subtitle,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: ColorTokens.primary,
),
],
),
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../unionflow_design_system.dart';
/// AppBar standardisé UnionFlow
///
/// Composant AppBar unifié pour toutes les pages de détail/formulaire.
/// Garantit la cohérence visuelle et l'expérience utilisateur.
class UFAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool automaticallyImplyLeading;
final PreferredSizeWidget? bottom;
final Color? backgroundColor;
final Color? foregroundColor;
final double elevation;
const UFAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.automaticallyImplyLeading = true,
this.bottom,
this.backgroundColor,
this.foregroundColor,
this.elevation = 0,
});
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(title),
backgroundColor: backgroundColor ?? ColorTokens.primary,
foregroundColor: foregroundColor ?? ColorTokens.onPrimary,
elevation: elevation,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
actions: actions,
bottom: bottom,
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light, // Icônes claires sur fond bleu
statusBarBrightness: Brightness.dark, // Pour iOS
),
centerTitle: false,
titleTextStyle: TypographyTokens.titleLarge.copyWith(
color: foregroundColor ?? ColorTokens.onPrimary,
fontWeight: FontWeight.w600,
),
);
}
@override
Size get preferredSize => Size.fromHeight(
kToolbarHeight + (bottom?.preferredSize.height ?? 0.0),
);
}

View File

@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Container standardisé UnionFlow
///
/// Composant Container unifié avec styles prédéfinis.
/// Garantit la cohérence des espacements, rayons et ombres.
///
/// Usage:
/// ```dart
/// UFContainer(
/// child: Text('Contenu'),
/// )
///
/// UFContainer.rounded(
/// color: ColorTokens.primary,
/// child: Text('Contenu'),
/// )
///
/// UFContainer.elevated(
/// child: Text('Contenu'),
/// )
/// ```
class UFContainer extends StatelessWidget {
final Widget child;
final Color? color;
final EdgeInsets? padding;
final EdgeInsets? margin;
final double? width;
final double? height;
final AlignmentGeometry? alignment;
final BoxConstraints? constraints;
final Gradient? gradient;
final double borderRadius;
final Border? border;
final List<BoxShadow>? boxShadow;
/// Container standard
const UFContainer({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusMd;
/// Container avec coins arrondis
const UFContainer.rounded({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusLg;
/// Container très arrondi
const UFContainer.extraRounded({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusXl;
/// Container avec ombre
UFContainer.elevated({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
}) : borderRadius = SpacingTokens.radiusLg,
boxShadow = ShadowTokens.sm;
/// Container circulaire
const UFContainer.circular({
super.key,
required this.child,
this.color,
this.padding,
this.margin,
this.width,
this.height,
this.alignment,
this.constraints,
this.gradient,
this.border,
this.boxShadow,
}) : borderRadius = SpacingTokens.radiusCircular;
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
padding: padding,
margin: margin,
alignment: alignment,
constraints: constraints,
decoration: BoxDecoration(
color: gradient == null ? (color ?? ColorTokens.surface) : null,
gradient: gradient,
borderRadius: BorderRadius.circular(borderRadius),
border: border,
boxShadow: boxShadow,
),
child: child,
);
}
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Header harmonisé UnionFlow
///
/// Composant header standardisé pour toutes les pages de l'application.
/// Garantit la cohérence visuelle et l'expérience utilisateur.
class UFHeader extends StatelessWidget {
final String title;
final String? subtitle;
final IconData icon;
final List<Widget>? actions;
final VoidCallback? onNotificationTap;
final VoidCallback? onSettingsTap;
final bool showActions;
const UFHeader({
super.key,
required this.title,
this.subtitle,
required this.icon,
this.actions,
this.onNotificationTap,
this.onSettingsTap,
this.showActions = true,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: ColorTokens.primaryGradient,
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.primary,
),
child: Row(
children: [
// Icône et contenu principal
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: ColorTokens.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: ColorTokens.onPrimary,
size: 24,
),
),
const SizedBox(width: SpacingTokens.lg),
// Titre et sous-titre
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onPrimary.withOpacity(0.8),
),
),
],
],
),
),
// Actions
if (showActions) _buildActions(),
],
),
);
}
Widget _buildActions() {
if (actions != null) {
return Row(children: actions!);
}
return Row(
children: [
if (onNotificationTap != null)
Container(
decoration: BoxDecoration(
color: ColorTokens.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onNotificationTap,
icon: const Icon(
Icons.notifications_outlined,
color: ColorTokens.onPrimary,
),
),
),
if (onNotificationTap != null && onSettingsTap != null)
const SizedBox(width: SpacingTokens.sm),
if (onSettingsTap != null)
Container(
decoration: BoxDecoration(
color: ColorTokens.onPrimary.withOpacity(0.2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
),
child: IconButton(
onPressed: onSettingsTap,
icon: const Icon(
Icons.settings_outlined,
color: ColorTokens.onPrimary,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,248 @@
import 'package:flutter/material.dart';
import '../unionflow_design_system.dart';
/// Header de page compact et moderne
///
/// Composant header minimaliste pour les pages principales du BottomNavigationBar.
/// Design épuré sans gradient lourd, optimisé pour l'espace.
///
/// Usage:
/// ```dart
/// UFPageHeader(
/// title: 'Membres',
/// icon: Icons.people,
/// actions: [
/// IconButton(icon: Icon(Icons.add), onPressed: () {}),
/// ],
/// )
/// ```
class UFPageHeader extends StatelessWidget {
final String title;
final IconData icon;
final List<Widget>? actions;
final Color? iconColor;
final bool showDivider;
const UFPageHeader({
super.key,
required this.title,
required this.icon,
this.actions,
this.iconColor,
this.showDivider = true,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.md,
),
child: Row(
children: [
// Icône
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: effectiveIconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 20,
),
),
const SizedBox(width: SpacingTokens.md),
// Titre
Expanded(
child: Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
),
// Actions
if (actions != null) ...actions!,
],
),
),
// Divider optionnel
if (showDivider)
Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.1),
),
],
);
}
}
/// Header de page avec statistiques
///
/// Header compact avec des métriques KPI intégrées.
///
/// Usage:
/// ```dart
/// UFPageHeaderWithStats(
/// title: 'Membres',
/// icon: Icons.people,
/// stats: [
/// UFHeaderStat(label: 'Total', value: '142'),
/// UFHeaderStat(label: 'Actifs', value: '128'),
/// ],
/// )
/// ```
class UFPageHeaderWithStats extends StatelessWidget {
final String title;
final IconData icon;
final List<UFHeaderStat> stats;
final List<Widget>? actions;
final Color? iconColor;
const UFPageHeaderWithStats({
super.key,
required this.title,
required this.icon,
required this.stats,
this.actions,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final effectiveIconColor = iconColor ?? ColorTokens.primary;
return Column(
children: [
// Titre et actions
Padding(
padding: const EdgeInsets.fromLTRB(
SpacingTokens.lg,
SpacingTokens.md,
SpacingTokens.lg,
SpacingTokens.sm,
),
child: Row(
children: [
// Icône
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: effectiveIconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
),
child: Icon(
icon,
color: effectiveIconColor,
size: 20,
),
),
const SizedBox(width: SpacingTokens.md),
// Titre
Expanded(
child: Text(
title,
style: TypographyTokens.titleLarge.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
),
// Actions
if (actions != null) ...actions!,
],
),
),
// Statistiques
Padding(
padding: const EdgeInsets.fromLTRB(
SpacingTokens.lg,
0,
SpacingTokens.lg,
SpacingTokens.md,
),
child: Row(
children: stats.map((stat) {
final isLast = stat == stats.last;
return Expanded(
child: Padding(
padding: EdgeInsets.only(
right: isLast ? 0 : SpacingTokens.sm,
),
child: _buildStatItem(stat),
),
);
}).toList(),
),
),
// Divider
Divider(
height: 1,
thickness: 1,
color: ColorTokens.outline.withOpacity(0.1),
),
],
);
}
Widget _buildStatItem(UFHeaderStat stat) {
return UFContainer.rounded(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
color: (stat.color ?? ColorTokens.primary).withOpacity(0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stat.value,
style: TypographyTokens.titleMedium.copyWith(
color: stat.color ?? ColorTokens.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
stat.label,
style: TypographyTokens.labelSmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
/// Statistique pour UFPageHeaderWithStats
class UFHeaderStat {
final String label;
final String value;
final Color? color;
const UFHeaderStat({
required this.label,
required this.value,
this.color,
});
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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';