Files
unionflow-client-quarkus-pr…/unionflow-mobile-apps/lib/shared/widgets/cards/unified_card_widget.dart
2025-09-17 17:54:06 +00:00

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,
}