import 'package:flutter/material.dart'; /// Bouton personnalisé moderne et élégant. /// /// Design épuré avec des proportions optimisées pour une application professionnelle. /// Supporte Material 3 avec des variantes tonal, outlined et text. /// /// **Usage:** /// ```dart /// CustomButton( /// text: 'Confirmer', /// onPressed: () {}, /// variant: ButtonVariant.primary, /// size: ButtonSize.medium, /// ) /// ``` class CustomButton extends StatelessWidget { const CustomButton({ required this.text, required this.onPressed, super.key, this.icon, this.isLoading = false, this.isEnabled = true, this.variant = ButtonVariant.primary, this.size = ButtonSize.medium, this.fullWidth = false, }); final String text; final VoidCallback onPressed; final IconData? icon; final bool isLoading; final bool isEnabled; final ButtonVariant variant; final ButtonSize size; final bool fullWidth; @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDisabled = !isEnabled || isLoading; final buttonStyle = _getButtonStyle(theme, variant, size); final textStyle = _getTextStyle(theme, variant, size); Widget child = isLoading ? SizedBox( height: size == ButtonSize.small ? 14 : (size == ButtonSize.medium ? 16 : 18), width: size == ButtonSize.small ? 14 : (size == ButtonSize.medium ? 16 : 18), child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( _getLoadingColor(theme, variant), ), ), ) : Row( mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) ...[ Icon(icon, size: _getIconSize(size)), SizedBox(width: size == ButtonSize.small ? 6 : 8), ], Text(text, style: textStyle), ], ); Widget button; switch (variant) { case ButtonVariant.primary: button = ElevatedButton( onPressed: isDisabled ? null : onPressed, style: buttonStyle, child: child, ); break; case ButtonVariant.tonal: button = FilledButton.tonal( onPressed: isDisabled ? null : onPressed, style: buttonStyle, child: child, ); break; case ButtonVariant.outlined: button = OutlinedButton( onPressed: isDisabled ? null : onPressed, style: buttonStyle, child: child, ); break; case ButtonVariant.text: button = TextButton( onPressed: isDisabled ? null : onPressed, style: buttonStyle, child: child, ); break; } return fullWidth ? SizedBox(width: double.infinity, child: button) : button; } ButtonStyle _getButtonStyle( ThemeData theme, ButtonVariant variant, ButtonSize size, ) { final padding = _getPadding(size); final borderRadius = BorderRadius.circular(_getBorderRadius(size)); final elevation = variant == ButtonVariant.primary ? 2.0 : 0.0; switch (variant) { case ButtonVariant.primary: return ElevatedButton.styleFrom( padding: padding, elevation: elevation, shape: RoundedRectangleBorder(borderRadius: borderRadius), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ); case ButtonVariant.tonal: return FilledButton.styleFrom( padding: padding, shape: RoundedRectangleBorder(borderRadius: borderRadius), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ); case ButtonVariant.outlined: return OutlinedButton.styleFrom( padding: padding, side: BorderSide( color: theme.colorScheme.outline, width: 1.5, ), shape: RoundedRectangleBorder(borderRadius: borderRadius), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ); case ButtonVariant.text: return TextButton.styleFrom( padding: padding, shape: RoundedRectangleBorder(borderRadius: borderRadius), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ); } } TextStyle _getTextStyle( ThemeData theme, ButtonVariant variant, ButtonSize size, ) { final fontSize = _getFontSize(size); final fontWeight = size == ButtonSize.small ? FontWeight.w500 : FontWeight.w600; Color textColor; switch (variant) { case ButtonVariant.primary: textColor = theme.colorScheme.onPrimary; break; case ButtonVariant.tonal: textColor = theme.colorScheme.onSecondaryContainer; break; case ButtonVariant.outlined: case ButtonVariant.text: textColor = theme.colorScheme.primary; break; } return TextStyle( fontSize: fontSize, fontWeight: fontWeight, color: textColor, letterSpacing: 0.1, ); } Color _getLoadingColor(ThemeData theme, ButtonVariant variant) { switch (variant) { case ButtonVariant.primary: return theme.colorScheme.onPrimary; case ButtonVariant.tonal: return theme.colorScheme.onSecondaryContainer; case ButtonVariant.outlined: case ButtonVariant.text: return theme.colorScheme.primary; } } EdgeInsets _getPadding(ButtonSize size) { switch (size) { case ButtonSize.small: return const EdgeInsets.symmetric(horizontal: 12, vertical: 6); case ButtonSize.medium: return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); case ButtonSize.large: return const EdgeInsets.symmetric(horizontal: 20, vertical: 12); } } double _getFontSize(ButtonSize size) { switch (size) { case ButtonSize.small: return 13; case ButtonSize.medium: return 14; case ButtonSize.large: return 15; } } double _getIconSize(ButtonSize size) { switch (size) { case ButtonSize.small: return 16; case ButtonSize.medium: return 18; case ButtonSize.large: return 20; } } double _getBorderRadius(ButtonSize size) { switch (size) { case ButtonSize.small: return 8; case ButtonSize.medium: return 10; case ButtonSize.large: return 12; } } } /// Variantes de boutons disponibles (Material 3). enum ButtonVariant { /// Bouton principal élevé (filled, elevated) primary, /// Bouton tonal (filled tonal - Material 3) tonal, /// Bouton avec bordure (outlined) outlined, /// Bouton texte sans fond (text) text, } /// Tailles de boutons disponibles. enum ButtonSize { /// Petite taille - compact small, /// Taille moyenne - standard medium, /// Grande taille - proéminente large, }