341 lines
8.6 KiB
Dart
341 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../theme/app_theme.dart';
|
|
|
|
/// Widget de carte unifié pour toute l'application
|
|
///
|
|
/// Fournit un design cohérent avec :
|
|
/// - Styles standardisés (élévation, bordures, couleurs)
|
|
/// - Support des animations hover et tap
|
|
/// - Variantes de style (elevated, outlined, filled)
|
|
/// - Gestion des états (loading, disabled)
|
|
class UnifiedCard extends StatefulWidget {
|
|
/// Contenu principal de la carte
|
|
final Widget child;
|
|
|
|
/// Callback lors du tap sur la carte
|
|
final VoidCallback? onTap;
|
|
|
|
/// Callback lors du long press
|
|
final VoidCallback? onLongPress;
|
|
|
|
/// Padding interne de la carte
|
|
final EdgeInsetsGeometry? padding;
|
|
|
|
/// Marge externe de la carte
|
|
final EdgeInsetsGeometry? margin;
|
|
|
|
/// Largeur de la carte
|
|
final double? width;
|
|
|
|
/// Hauteur de la carte
|
|
final double? height;
|
|
|
|
/// Variante de style de la carte
|
|
final UnifiedCardVariant variant;
|
|
|
|
/// Couleur de fond personnalisée
|
|
final Color? backgroundColor;
|
|
|
|
/// Couleur de bordure personnalisée
|
|
final Color? borderColor;
|
|
|
|
/// Indique si la carte est désactivée
|
|
final bool disabled;
|
|
|
|
/// Indique si la carte est en cours de chargement
|
|
final bool loading;
|
|
|
|
/// Élévation personnalisée
|
|
final double? elevation;
|
|
|
|
/// Rayon des bordures personnalisé
|
|
final double? borderRadius;
|
|
|
|
const UnifiedCard({
|
|
super.key,
|
|
required this.child,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.padding,
|
|
this.margin,
|
|
this.width,
|
|
this.height,
|
|
this.variant = UnifiedCardVariant.elevated,
|
|
this.backgroundColor,
|
|
this.borderColor,
|
|
this.disabled = false,
|
|
this.loading = false,
|
|
this.elevation,
|
|
this.borderRadius,
|
|
});
|
|
|
|
/// Constructeur pour une carte élevée
|
|
const UnifiedCard.elevated({
|
|
super.key,
|
|
required this.child,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.padding,
|
|
this.margin,
|
|
this.width,
|
|
this.height,
|
|
this.backgroundColor,
|
|
this.disabled = false,
|
|
this.loading = false,
|
|
this.elevation,
|
|
this.borderRadius,
|
|
}) : variant = UnifiedCardVariant.elevated,
|
|
borderColor = null;
|
|
|
|
/// Constructeur pour une carte avec bordure
|
|
const UnifiedCard.outlined({
|
|
super.key,
|
|
required this.child,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.padding,
|
|
this.margin,
|
|
this.width,
|
|
this.height,
|
|
this.backgroundColor,
|
|
this.borderColor,
|
|
this.disabled = false,
|
|
this.loading = false,
|
|
this.elevation,
|
|
this.borderRadius,
|
|
}) : variant = UnifiedCardVariant.outlined;
|
|
|
|
/// Constructeur pour une carte remplie
|
|
const UnifiedCard.filled({
|
|
super.key,
|
|
required this.child,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.padding,
|
|
this.margin,
|
|
this.width,
|
|
this.height,
|
|
this.backgroundColor,
|
|
this.borderColor,
|
|
this.disabled = false,
|
|
this.loading = false,
|
|
this.elevation,
|
|
this.borderRadius,
|
|
}) : variant = UnifiedCardVariant.filled;
|
|
|
|
/// Constructeur pour une carte KPI
|
|
const UnifiedCard.kpi({
|
|
super.key,
|
|
required this.child,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.margin,
|
|
this.width,
|
|
this.height,
|
|
this.backgroundColor,
|
|
this.disabled = false,
|
|
this.loading = false,
|
|
}) : variant = UnifiedCardVariant.elevated,
|
|
padding = const EdgeInsets.all(20),
|
|
borderColor = null,
|
|
elevation = 2,
|
|
borderRadius = 16;
|
|
|
|
/// Constructeur pour une carte de liste
|
|
const UnifiedCard.listItem({
|
|
super.key,
|
|
required this.child,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.margin,
|
|
this.width,
|
|
this.height,
|
|
this.backgroundColor,
|
|
this.disabled = false,
|
|
this.loading = false,
|
|
}) : variant = UnifiedCardVariant.outlined,
|
|
padding = const EdgeInsets.all(16),
|
|
borderColor = null,
|
|
elevation = 0,
|
|
borderRadius = 12;
|
|
|
|
@override
|
|
State<UnifiedCard> createState() => _UnifiedCardState();
|
|
}
|
|
|
|
class _UnifiedCardState extends State<UnifiedCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late Animation<double> _scaleAnimation;
|
|
late Animation<double> _elevationAnimation;
|
|
bool _isHovered = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
duration: const Duration(milliseconds: 150),
|
|
vsync: this,
|
|
);
|
|
|
|
_scaleAnimation = Tween<double>(
|
|
begin: 1.0,
|
|
end: 0.98,
|
|
).animate(CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
|
|
_elevationAnimation = Tween<double>(
|
|
begin: _getBaseElevation(),
|
|
end: _getBaseElevation() + 2,
|
|
).animate(CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
double _getBaseElevation() {
|
|
if (widget.elevation != null) return widget.elevation!;
|
|
switch (widget.variant) {
|
|
case UnifiedCardVariant.elevated:
|
|
return 2;
|
|
case UnifiedCardVariant.outlined:
|
|
return 0;
|
|
case UnifiedCardVariant.filled:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
Color _getBackgroundColor() {
|
|
if (widget.backgroundColor != null) return widget.backgroundColor!;
|
|
if (widget.disabled) return AppTheme.surfaceVariant.withOpacity(0.5);
|
|
|
|
switch (widget.variant) {
|
|
case UnifiedCardVariant.elevated:
|
|
return Colors.white;
|
|
case UnifiedCardVariant.outlined:
|
|
return Colors.white;
|
|
case UnifiedCardVariant.filled:
|
|
return AppTheme.surfaceVariant;
|
|
}
|
|
}
|
|
|
|
Border? _getBorder() {
|
|
if (widget.variant == UnifiedCardVariant.outlined) {
|
|
return Border.all(
|
|
color: widget.borderColor ?? AppTheme.outline,
|
|
width: 1,
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget card = AnimatedBuilder(
|
|
animation: _animationController,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: Container(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
margin: widget.margin,
|
|
decoration: BoxDecoration(
|
|
color: _getBackgroundColor(),
|
|
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12),
|
|
border: _getBorder(),
|
|
boxShadow: widget.variant == UnifiedCardVariant.elevated
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: _elevationAnimation.value * 2,
|
|
offset: Offset(0, _elevationAnimation.value),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: widget.loading
|
|
? _buildLoadingState()
|
|
: Padding(
|
|
padding: widget.padding ?? const EdgeInsets.all(16),
|
|
child: widget.child,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
if (widget.onTap != null && !widget.disabled && !widget.loading) {
|
|
card = MouseRegion(
|
|
onEnter: (_) => _onHover(true),
|
|
onExit: (_) => _onHover(false),
|
|
child: GestureDetector(
|
|
onTap: widget.onTap,
|
|
onLongPress: widget.onLongPress,
|
|
onTapDown: (_) => _animationController.forward(),
|
|
onTapUp: (_) => _animationController.reverse(),
|
|
onTapCancel: () => _animationController.reverse(),
|
|
child: card,
|
|
),
|
|
);
|
|
}
|
|
|
|
return card;
|
|
}
|
|
|
|
void _onHover(bool isHovered) {
|
|
if (mounted && !widget.disabled && !widget.loading) {
|
|
setState(() {
|
|
_isHovered = isHovered;
|
|
});
|
|
if (isHovered) {
|
|
_animationController.forward();
|
|
} else {
|
|
_animationController.reverse();
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildLoadingState() {
|
|
return Container(
|
|
padding: widget.padding ?? const EdgeInsets.all(16),
|
|
child: const Center(
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Variantes de style pour les cartes unifiées
|
|
enum UnifiedCardVariant {
|
|
/// Carte avec élévation et ombre
|
|
elevated,
|
|
|
|
/// Carte avec bordure uniquement
|
|
outlined,
|
|
|
|
/// Carte avec fond coloré
|
|
filled,
|
|
}
|