370 lines
10 KiB
Dart
370 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Design tokens for DayMaker — a single source of truth for the new look.
|
|
class AppColors {
|
|
// Brand
|
|
static const Color primary = Color(0xFF2563EB); // refined modern blue
|
|
static const Color primaryDark = Color(0xFF1D4ED8);
|
|
static const Color primaryLight = Color(0xFF60A5FA);
|
|
static const Color accent = Color(0xFFFB923C); // warm orange accent (legacy peach reborn)
|
|
|
|
// Neutrals
|
|
static const Color background = Color(0xFFF7F8FB);
|
|
static const Color surface = Colors.white;
|
|
static const Color surfaceAlt = Color(0xFFF1F5F9);
|
|
static const Color border = Color(0xFFE2E8F0);
|
|
static const Color divider = Color(0xFFEDF2F7);
|
|
|
|
// Text
|
|
static const Color textPrimary = Color(0xFF0F172A);
|
|
static const Color textSecondary = Color(0xFF64748B);
|
|
static const Color textTertiary = Color(0xFF94A3B8);
|
|
static const Color textOnPrimary = Colors.white;
|
|
|
|
// Status
|
|
static const Color success = Color(0xFF10B981);
|
|
static const Color error = Color(0xFFEF4444);
|
|
static const Color warning = Color(0xFFF59E0B);
|
|
|
|
// Gradients
|
|
static const LinearGradient brandGradient = LinearGradient(
|
|
colors: [Color(0xFF2563EB), Color(0xFF60A5FA)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
);
|
|
|
|
static const LinearGradient warmGradient = LinearGradient(
|
|
colors: [Color(0xFFFB923C), Color(0xFFF472B6)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
);
|
|
}
|
|
|
|
class AppRadius {
|
|
static const double sm = 8;
|
|
static const double md = 12;
|
|
static const double lg = 16;
|
|
static const double xl = 20;
|
|
static const double pill = 999;
|
|
}
|
|
|
|
class AppSpacing {
|
|
static const double xs = 4;
|
|
static const double sm = 8;
|
|
static const double md = 12;
|
|
static const double lg = 16;
|
|
static const double xl = 20;
|
|
static const double xxl = 24;
|
|
static const double huge = 32;
|
|
}
|
|
|
|
class AppShadows {
|
|
static List<BoxShadow> soft = [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.04),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
];
|
|
|
|
static List<BoxShadow> medium = [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.06),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
];
|
|
|
|
static List<BoxShadow> brand = [
|
|
BoxShadow(
|
|
color: AppColors.primary.withValues(alpha: 0.25),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
];
|
|
}
|
|
|
|
class AppText {
|
|
static const TextStyle h1 = TextStyle(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.textPrimary,
|
|
height: 1.2,
|
|
);
|
|
|
|
static const TextStyle h2 = TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.textPrimary,
|
|
height: 1.25,
|
|
);
|
|
|
|
static const TextStyle h3 = TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.textPrimary,
|
|
height: 1.3,
|
|
);
|
|
|
|
static const TextStyle body = TextStyle(
|
|
fontSize: 15,
|
|
color: AppColors.textPrimary,
|
|
height: 1.4,
|
|
);
|
|
|
|
static const TextStyle bodySecondary = TextStyle(
|
|
fontSize: 14,
|
|
color: AppColors.textSecondary,
|
|
height: 1.4,
|
|
);
|
|
|
|
static const TextStyle caption = TextStyle(
|
|
fontSize: 12,
|
|
color: AppColors.textTertiary,
|
|
height: 1.3,
|
|
);
|
|
|
|
static const TextStyle button = TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
letterSpacing: 0.2,
|
|
);
|
|
|
|
static const TextStyle label = TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textSecondary,
|
|
letterSpacing: 0.3,
|
|
);
|
|
}
|
|
|
|
/// Shared decorations for cards and surfaces.
|
|
class AppDecorations {
|
|
static BoxDecoration card({Color? color, double radius = AppRadius.lg}) =>
|
|
BoxDecoration(
|
|
color: color ?? AppColors.surface,
|
|
borderRadius: BorderRadius.circular(radius),
|
|
boxShadow: AppShadows.soft,
|
|
);
|
|
|
|
static BoxDecoration outlined({double radius = AppRadius.lg}) =>
|
|
BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: BorderRadius.circular(radius),
|
|
border: Border.all(color: AppColors.border),
|
|
);
|
|
|
|
static BoxDecoration filled({Color? color, double radius = AppRadius.md}) =>
|
|
BoxDecoration(
|
|
color: color ?? AppColors.surfaceAlt,
|
|
borderRadius: BorderRadius.circular(radius),
|
|
);
|
|
}
|
|
|
|
/// Reusable widgets.
|
|
class AppButton extends StatelessWidget {
|
|
final String label;
|
|
final IconData? icon;
|
|
final VoidCallback? onPressed;
|
|
final bool loading;
|
|
final bool secondary;
|
|
final bool danger;
|
|
final double height;
|
|
|
|
const AppButton({
|
|
super.key,
|
|
required this.label,
|
|
this.icon,
|
|
this.onPressed,
|
|
this.loading = false,
|
|
this.secondary = false,
|
|
this.danger = false,
|
|
this.height = 54,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final disabled = onPressed == null || loading;
|
|
final bg = danger
|
|
? AppColors.error
|
|
: secondary
|
|
? AppColors.surface
|
|
: AppColors.primary;
|
|
final fg = secondary ? AppColors.primary : Colors.white;
|
|
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: height,
|
|
child: AnimatedOpacity(
|
|
opacity: disabled && !loading ? 0.5 : 1,
|
|
duration: const Duration(milliseconds: 150),
|
|
child: Material(
|
|
color: bg,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
elevation: secondary ? 0 : 0,
|
|
child: InkWell(
|
|
onTap: disabled ? null : onPressed,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
border: secondary
|
|
? Border.all(color: AppColors.primary, width: 1.5)
|
|
: null,
|
|
boxShadow: secondary || danger ? null : AppShadows.brand,
|
|
),
|
|
alignment: Alignment.center,
|
|
child: loading
|
|
? SizedBox(
|
|
height: 22,
|
|
width: 22,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2.4,
|
|
valueColor: AlwaysStoppedAnimation<Color>(fg),
|
|
),
|
|
)
|
|
: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(icon, color: fg, size: 20),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Text(
|
|
label,
|
|
style: AppText.button.copyWith(color: fg),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Pill-style chip for filters/tags. Animated selection.
|
|
class AppChip extends StatelessWidget {
|
|
final String label;
|
|
final IconData? icon;
|
|
final bool selected;
|
|
final VoidCallback? onTap;
|
|
final Color? color;
|
|
|
|
const AppChip({
|
|
super.key,
|
|
required this.label,
|
|
this.icon,
|
|
this.selected = false,
|
|
this.onTap,
|
|
this.color,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final accent = color ?? AppColors.primary;
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 180),
|
|
curve: Curves.easeOut,
|
|
decoration: BoxDecoration(
|
|
color: selected ? accent : AppColors.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.pill),
|
|
border: Border.all(
|
|
color: selected ? accent : AppColors.border,
|
|
width: 1.2,
|
|
),
|
|
boxShadow: selected
|
|
? [
|
|
BoxShadow(
|
|
color: accent.withValues(alpha: 0.25),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
)
|
|
]
|
|
: null,
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(AppRadius.pill),
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 14,
|
|
vertical: 8,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(
|
|
icon,
|
|
size: 16,
|
|
color: selected ? Colors.white : accent,
|
|
),
|
|
const SizedBox(width: 6),
|
|
],
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w600,
|
|
color: selected ? Colors.white : AppColors.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Helpers for snackbars
|
|
class AppSnack {
|
|
static void error(BuildContext context, String message) {
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentSnackBar()
|
|
..showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.error_outline, color: Colors.white),
|
|
const SizedBox(width: 10),
|
|
Expanded(child: Text(message)),
|
|
],
|
|
),
|
|
backgroundColor: AppColors.error,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static void success(BuildContext context, String message) {
|
|
ScaffoldMessenger.of(context)
|
|
..hideCurrentSnackBar()
|
|
..showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.check_circle_outline, color: Colors.white),
|
|
const SizedBox(width: 10),
|
|
Expanded(child: Text(message)),
|
|
],
|
|
),
|
|
backgroundColor: AppColors.success,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|