feat(shared): legacy presentation/ + shared design system + widgets

- lib/presentation : pages legacy (explore/network, notifications) avec BLoC
- lib/shared/design_system : UnionFlow Design System v2 (tokens, components)
  + MD3 tokens + module_colors par feature
- lib/shared/widgets : widgets transversaux (core_card, core_shimmer,
  error_widget, loading_widget, powered_by_lions_dev, etc.)
- lib/shared/constants + utils
This commit is contained in:
dahoud
2026-04-15 20:27:23 +00:00
parent 744faa3a9c
commit 7cd7c6fc9e
36 changed files with 1890 additions and 837 deletions

View File

@@ -33,7 +33,7 @@ class ActionRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final iconColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight;
final iconColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return Padding(
padding: const EdgeInsets.only(top: 8.0),
@@ -75,18 +75,18 @@ class ActionRow extends StatelessWidget {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.1),
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
if (customActionIcon != null) ...[
Icon(customActionIcon, size: 14, color: AppColors.primaryGreen),
Icon(customActionIcon, size: 14, color: AppColors.primary),
const SizedBox(width: 4),
],
Text(
customActionLabel ?? '',
style: AppTypography.badgeText.copyWith(color: AppColors.primaryGreen),
style: AppTypography.badgeText.copyWith(color: AppColors.primary),
),
],
),

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import '../design_system/tokens/app_colors.dart';
/// UnionFlow Mobile - Composant DRY Centralisé : CoreCard
/// Le seul et unique conteneur d'affichage (Posts, Événements, Profils).
@@ -22,21 +21,21 @@ class CoreCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final scheme = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
margin: margin,
decoration: BoxDecoration(
color: backgroundColor ?? (isDark ? AppColors.darkSurface : Colors.white),
color: backgroundColor ?? scheme.surface,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: isDark ? AppColors.darkBorder.withOpacity(0.5) : AppColors.lightBorder,
color: scheme.outlineVariant.withOpacity(0.6),
width: 0.8,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDark ? 0.15 : 0.04),
color: scheme.shadow.withOpacity(0.06),
blurRadius: 6,
offset: const Offset(0, 2),
),

View File

@@ -37,32 +37,32 @@ class CoreTextField extends StatelessWidget {
decoration: InputDecoration(
hintText: hintText,
hintStyle: AppTypography.subtitleSmall.copyWith(
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
),
prefixIcon: prefixIcon != null
? Icon(prefixIcon, size: 20, color: AppColors.primaryGreen)
? Icon(prefixIcon, size: 20, color: AppColors.primary)
: null,
filled: true,
fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
fillColor: isDark ? AppColors.surfaceDark : AppColors.surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: isDark ? AppColors.darkBorder : AppColors.lightBorder,
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: isDark ? AppColors.darkBorder : AppColors.lightBorder,
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.primaryGreen,
color: AppColors.primary,
width: 2,
),
),

View File

@@ -21,7 +21,7 @@ class DynamicFAB extends StatelessWidget {
if (label != null) {
return FloatingActionButton.extended(
onPressed: onPressed,
backgroundColor: AppColors.primaryGreen,
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 4,
icon: Icon(icon, size: 20),
@@ -34,7 +34,7 @@ class DynamicFAB extends StatelessWidget {
return FloatingActionButton(
onPressed: onPressed,
backgroundColor: AppColors.primaryGreen,
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 4,
child: Icon(icon, size: 24),

View File

@@ -3,6 +3,7 @@ library error_display_widget;
import 'package:flutter/material.dart';
import '../../core/error/failures.dart';
import '../design_system/tokens/app_colors.dart';
/// Error display widget that shows failures in a user-friendly way
class ErrorDisplayWidget extends StatelessWidget {
@@ -48,7 +49,7 @@ class ErrorDisplayWidget extends StatelessWidget {
Text(
failure.getUserMessage(),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
@@ -98,15 +99,15 @@ class ErrorDisplayWidget extends StatelessWidget {
/// Get appropriate color for error type
Color _getErrorColor(BuildContext context) {
if (failure is NetworkFailure) {
return Colors.orange;
return AppColors.warning;
} else if (failure is UnauthorizedFailure) {
return Colors.red;
return AppColors.error;
} else if (failure is ForbiddenFailure) {
return Colors.deepOrange;
return AppColors.warning;
} else if (failure is ValidationFailure) {
return Colors.amber;
return AppColors.warningUI;
} else if (failure is NotImplementedFailure) {
return Colors.blue[700]!;
return AppColors.info;
} else {
return Theme.of(context).colorScheme.error;
}
@@ -183,7 +184,7 @@ class ErrorBanner extends StatelessWidget {
failure.getUserMessage(),
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
@@ -228,15 +229,15 @@ class ErrorBanner extends StatelessWidget {
Color _getErrorColor(BuildContext context) {
if (failure is NetworkFailure) {
return Colors.orange;
return AppColors.warning;
} else if (failure is UnauthorizedFailure) {
return Colors.red;
return AppColors.error;
} else if (failure is ForbiddenFailure) {
return Colors.deepOrange;
return AppColors.warning;
} else if (failure is ValidationFailure) {
return Colors.amber;
return AppColors.warningUI;
} else if (failure is NotImplementedFailure) {
return Colors.blue[700]!;
return AppColors.info;
} else {
return Theme.of(context).colorScheme.error;
}
@@ -282,7 +283,7 @@ void showErrorSnackBar(
),
],
),
backgroundColor: failure is NetworkFailure ? Colors.orange : Colors.red,
backgroundColor: failure is NetworkFailure ? AppColors.warning : AppColors.error,
behavior: SnackBarBehavior.floating,
action: failure.isRetryable && onRetry != null
? SnackBarAction(

View File

@@ -13,7 +13,7 @@ class InfoBadge extends StatelessWidget {
const InfoBadge({
Key? key,
required this.text,
this.backgroundColor = AppColors.brandGreenLight,
this.backgroundColor = AppColors.primaryLight,
this.textColor = Colors.white,
this.icon,
}) : super(key: key);

View File

@@ -67,12 +67,12 @@ class ShimmerListLoading extends StatelessWidget {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
baseColor: Theme.of(context).colorScheme.surfaceContainerHighest,
highlightColor: Theme.of(context).colorScheme.surface,
child: Container(
height: itemHeight,
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
),
),
@@ -97,13 +97,13 @@ class ShimmerCardLoading extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
baseColor: Theme.of(context).colorScheme.surfaceContainerHighest,
highlightColor: Theme.of(context).colorScheme.surface,
child: Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
),
@@ -137,11 +137,11 @@ class ShimmerGridLoading extends StatelessWidget {
itemCount: itemCount,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
baseColor: Theme.of(context).colorScheme.surfaceContainerHighest,
highlightColor: Theme.of(context).colorScheme.surface,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
),
@@ -158,8 +158,8 @@ class ShimmerDetailLoading extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
baseColor: Theme.of(context).colorScheme.surfaceContainerHighest,
highlightColor: Theme.of(context).colorScheme.surface,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
@@ -169,7 +169,7 @@ class ShimmerDetailLoading extends StatelessWidget {
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
),
@@ -178,14 +178,14 @@ class ShimmerDetailLoading extends StatelessWidget {
Container(
height: 24,
width: double.infinity,
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
),
const SizedBox(height: 8),
// Subtitle
Container(
height: 16,
width: 200,
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
),
const SizedBox(height: 12),
// Content lines
@@ -195,7 +195,7 @@ class ShimmerDetailLoading extends StatelessWidget {
child: Container(
height: 12,
width: double.infinity,
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
),
);
}),

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../design_system/unionflow_design_system.dart';
/// Widget "Powered by Lions Dev" — affiche le logo Lions Dev adaptatif (dark/light)
/// avec lien cliquable vers https://www.lions.dev
///
/// Usage:
/// ```dart
/// const PoweredByLionsDev()
/// // ou compact:
/// const PoweredByLionsDev(compact: true)
/// ```
class PoweredByLionsDev extends StatelessWidget {
/// Si true, affichage compact (logo plus petit, sans label "Powered by")
final bool compact;
/// Couleur du label "Powered by" (par défaut : couleur secondaire du thème)
final Color? labelColor;
/// Hauteur du logo (par défaut : 28 normal, 20 compact)
final double? logoHeight;
/// Force une variante (utile sur fond toujours sombre/clair comme login).
/// Si null, suit le thème courant.
final Brightness? forceBrightness;
const PoweredByLionsDev({
super.key,
this.compact = false,
this.labelColor,
this.logoHeight,
this.forceBrightness,
});
Future<void> _openLionsDev() async {
final uri = Uri.parse('https://www.lions.dev');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
@override
Widget build(BuildContext context) {
final brightness = forceBrightness ?? Theme.of(context).brightness;
final isDark = brightness == Brightness.dark;
// Logo blanc sur fond sombre, logo noir sur fond clair
final logoAsset = isDark
? 'assets/images/branding/lions_dev_white.png'
: 'assets/images/branding/lions_dev_dark.png';
final effectiveLabelColor = labelColor ??
(isDark ? AppColors.textSecondaryDark : AppColors.textSecondary);
final effectiveHeight = logoHeight ?? (compact ? 20.0 : 28.0);
return InkWell(
onTap: _openLionsDev,
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!compact) ...[
Text(
'Powered by',
style: TextStyle(
fontSize: 11,
color: effectiveLabelColor,
letterSpacing: 0.3,
),
),
const SizedBox(width: SpacingTokens.sm),
],
Image.asset(
logoAsset,
height: effectiveHeight,
fit: BoxFit.contain,
),
],
),
),
);
}
}

View File

@@ -3,6 +3,7 @@ library validated_text_field;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../design_system/tokens/app_colors.dart';
/// Validated text field with consistent styling and behavior
class ValidatedTextField extends StatelessWidget {
@@ -61,6 +62,7 @@ class ValidatedTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return TextFormField(
controller: controller,
initialValue: initialValue,
@@ -73,30 +75,32 @@ class ValidatedTextField extends StatelessWidget {
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.grey.shade400,
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1.0,
),
),
focusedBorder: const OutlineInputBorder(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blue,
color: AppColors.primary,
width: 2.0,
),
),
errorBorder: const OutlineInputBorder(
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.red,
color: AppColors.error,
width: 1.0,
),
),
focusedErrorBorder: const OutlineInputBorder(
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.red,
color: AppColors.error,
width: 2.0,
),
),
filled: !enabled,
fillColor: !enabled ? Colors.grey.shade100 : null,
fillColor: !enabled
? (isDark ? AppColors.surfaceVariantDark : AppColors.backgroundSubtle)
: null,
counterText: showCounter ? null : '',
),
validator: validator,
@@ -165,10 +169,10 @@ class ValidatedAmountField extends StatelessWidget {
padding: const EdgeInsets.all(12.0),
child: Text(
currencySymbol,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey,
color: AppColors.textTertiary,
),
),
),
@@ -206,6 +210,7 @@ class ValidatedDropdownField<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return DropdownButtonFormField<T>(
value: value,
items: items,
@@ -216,24 +221,26 @@ class ValidatedDropdownField<T> extends StatelessWidget {
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.grey.shade400,
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1.0,
),
),
focusedBorder: const OutlineInputBorder(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blue,
color: AppColors.primary,
width: 2.0,
),
),
errorBorder: const OutlineInputBorder(
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.red,
color: AppColors.error,
width: 1.0,
),
),
filled: !enabled,
fillColor: !enabled ? Colors.grey.shade100 : null,
fillColor: !enabled
? (isDark ? AppColors.surfaceVariantDark : AppColors.backgroundSubtle)
: null,
),
validator: validator,
onChanged: enabled ? onChanged : null,
@@ -269,6 +276,7 @@ class ValidatedDateField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return InkWell(
onTap: enabled
? () async {
@@ -292,24 +300,26 @@ class ValidatedDateField extends StatelessWidget {
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.grey.shade400,
color: isDark ? AppColors.borderDark : AppColors.border,
width: 1.0,
),
),
focusedBorder: const OutlineInputBorder(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blue,
color: AppColors.primary,
width: 2.0,
),
),
errorBorder: const OutlineInputBorder(
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.red,
color: AppColors.error,
width: 1.0,
),
),
filled: !enabled,
fillColor: !enabled ? Colors.grey.shade100 : null,
fillColor: !enabled
? (isDark ? AppColors.surfaceVariantDark : AppColors.backgroundSubtle)
: null,
errorText: validator != null ? validator!(selectedDate) : null,
),
child: Text(
@@ -317,7 +327,9 @@ class ValidatedDateField extends StatelessWidget {
? '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}'
: hintText ?? 'Sélectionner une date',
style: TextStyle(
color: selectedDate != null ? Colors.black87 : Colors.grey,
color: selectedDate != null
? (isDark ? AppColors.textPrimaryDark : AppColors.textPrimary)
: AppColors.textTertiary,
),
),
),