Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ publish_to: 'none'
version: 0.1.0

environment:
#sdk: ^3.6.0
sdk: ^3.10.7

dependencies:
flutter:
sdk: flutter
mbvn_flutter_base:
path: ../

flutter:
uses-material-design: true
20 changes: 20 additions & 0 deletions lib/src/design_system/app_radius.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';

/// Border radius scale constants for the design system.
abstract final class AppRadius {
AppRadius._();

// Raw double values
static const double xs = 4;
static const double sm = 8;
static const double md = 16;
static const double lg = 24;
static const double full = 9999;

// Pre-built BorderRadius (all corners, const)
static const BorderRadius allXs = BorderRadius.all(Radius.circular(xs));
static const BorderRadius allSm = BorderRadius.all(Radius.circular(sm));
static const BorderRadius allMd = BorderRadius.all(Radius.circular(md));
static const BorderRadius allLg = BorderRadius.all(Radius.circular(lg));
static const BorderRadius allFull = BorderRadius.all(Radius.circular(full));
}
1 change: 1 addition & 0 deletions lib/src/design_system/design_system.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Placeholder - will implement in step 4
export 'spacing.dart';
export 'app_radius.dart';
export 'app_colors.dart';
export 'app_typography.dart';
export 'app_theme.dart';
Expand Down
45 changes: 45 additions & 0 deletions lib/src/widgets/animation_helpers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';

/// Standard animation durations.
abstract final class AnimationDurations {
AnimationDurations._();

static const Duration fast = Duration(milliseconds: 150);
static const Duration normal = Duration(milliseconds: 300);
static const Duration slow = Duration(milliseconds: 500);
}

/// Standard animation curves.
abstract final class AnimationCurves {
AnimationCurves._();

static const Curve standard = Curves.easeInOut;
static const Curve enter = Curves.easeOut;
static const Curve exit = Curves.easeIn;
}

/// Animated visibility wrapper that fades in/out its child.
class AnimatedVisibility extends StatelessWidget {
const AnimatedVisibility({
super.key,
required this.visible,
required this.child,
this.duration,
this.curve,
});

final bool visible;
final Widget child;
final Duration? duration;
final Curve? curve;

@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: duration ?? AnimationDurations.normal,
curve: curve ?? AnimationCurves.standard,
child: visible ? child : const SizedBox.shrink(),
);
}
}
55 changes: 55 additions & 0 deletions lib/src/widgets/app_avatar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';

/// Avatar widget with image, initials, or icon variants.
class AppAvatar extends StatelessWidget {
const AppAvatar({
super.key,
this.imageUrl,
this.initials,
this.icon,
this.radius = 20,
this.backgroundColor,
});

final String? imageUrl;
final String? initials;
final IconData? icon;
final double radius;
final Color? backgroundColor;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final bg = backgroundColor ?? theme.colorScheme.primaryContainer;
final fg = theme.colorScheme.onPrimaryContainer;

if (imageUrl != null) {
return CircleAvatar(
radius: radius,
backgroundImage: NetworkImage(imageUrl!),
backgroundColor: bg,
);
}

if (initials != null) {
return CircleAvatar(
radius: radius,
backgroundColor: bg,
child: Text(
initials!.toUpperCase(),
style: TextStyle(
color: fg,
fontSize: radius * 0.8,
fontWeight: FontWeight.w600,
),
),
);
}

return CircleAvatar(
radius: radius,
backgroundColor: bg,
child: Icon(icon ?? Icons.person, color: fg, size: radius),
);
}
}
84 changes: 84 additions & 0 deletions lib/src/widgets/app_badge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';

import '../design_system/design_system.dart';

/// Badge overlay for notifications, counters, and status indicators.
///
/// Wraps a [child] widget with a positioned badge.
/// Shows a dot badge when no [label] is provided.
/// Animates show/hide with [AnimatedOpacity].
class AppBadge extends StatelessWidget {
const AppBadge({
super.key,
required this.child,
this.label,
this.show = true,
this.backgroundColor,
this.alignment = Alignment.topRight,
});

final Widget child;
final String? label;
final bool show;
final Color? backgroundColor;
final Alignment alignment;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final bgColor = backgroundColor ?? theme.colorScheme.error;
final fgColor = theme.colorScheme.onError;

final isDot = label == null;

final badge = AnimatedOpacity(
opacity: show ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Container(
padding: isDot
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: Spacing.xs + 2,
vertical: 1,
),
constraints: BoxConstraints(
minWidth: isDot ? 8 : 16,
minHeight: isDot ? 8 : 16,
),
decoration: BoxDecoration(
color: bgColor,
borderRadius: isDot ? AppRadius.allXs : AppRadius.allSm,
),
child: isDot
? null
: Text(
label!,
style: TextStyle(
color: fgColor,
fontSize: 10,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
);

// Calculate offset based on alignment
final isTop = alignment.y <= 0;
final isRight = alignment.x >= 0;

return Stack(
clipBehavior: Clip.none,
children: [
child,
Positioned(
top: isTop ? -4 : null,
bottom: isTop ? null : -4,
right: isRight ? -4 : null,
left: isRight ? null : -4,
child: badge,
),
],
);
}
}
94 changes: 94 additions & 0 deletions lib/src/widgets/app_banner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';

import '../design_system/design_system.dart';

/// Semantic variant for [AppBanner].
enum BannerVariant { success, error, warning, info }

/// Persistent banner for informational or warning messages.
///
/// Displays at the top of content with semantic color variants
/// and optional action and dismiss button.
class AppBanner extends StatelessWidget {
const AppBanner({
super.key,
required this.message,
this.variant = BannerVariant.info,
this.action,
this.onDismiss,
});

final String message;
final BannerVariant variant;
final Widget? action;
final VoidCallback? onDismiss;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = _resolveColors(theme);

return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm + Spacing.xs,
),
decoration: BoxDecoration(
color: colors.$1,
borderRadius: AppRadius.allSm,
),
child: Row(
children: [
Icon(_resolveIcon(), size: 20, color: colors.$2),
const SizedBox(width: Spacing.sm),
Expanded(
child: Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: colors.$2,
),
),
),
if (action != null) ...[
const SizedBox(width: Spacing.sm),
action!,
],
if (onDismiss != null) ...[
const SizedBox(width: Spacing.xs),
GestureDetector(
onTap: onDismiss,
child: Icon(Icons.close, size: 18, color: colors.$2),
),
],
],
),
);
}

IconData _resolveIcon() {
switch (variant) {
case BannerVariant.success:
return Icons.check_circle_outline;
case BannerVariant.error:
return Icons.error_outline;
case BannerVariant.warning:
return Icons.warning_amber_rounded;
case BannerVariant.info:
return Icons.info_outline;
}
}

(Color background, Color foreground) _resolveColors(ThemeData theme) {
switch (variant) {
case BannerVariant.success:
return (const Color(0xFF16A34A).withValues(alpha: 0.12), const Color(0xFF16A34A));
case BannerVariant.error:
return (theme.colorScheme.error.withValues(alpha: 0.12), theme.colorScheme.error);
case BannerVariant.warning:
return (const Color(0xFFF59E0B).withValues(alpha: 0.12), const Color(0xFFB45309));
case BannerVariant.info:
return (theme.colorScheme.primary.withValues(alpha: 0.12), theme.colorScheme.primary);
}
}
}
Loading