1457 lines
39 KiB
Markdown
1457 lines
39 KiB
Markdown
# UI Design Guidelines - AI Study Assistant
|
|
|
|
## 🎨 DESIGN SYSTEM OVERVIEW
|
|
|
|
### Core Design Philosophy
|
|
- **Clean & Modern**: Minimalist interface with purposeful use of color and space
|
|
- **Educational Focus**: UI elements designed to support learning, not distract
|
|
- **Accessibility First**: High contrast ratios, readable fonts, and clear visual hierarchy
|
|
- **Responsive Design**: Seamless experience across mobile, tablet, and web
|
|
|
|
---
|
|
|
|
## 🎨 COLOR PALETTE
|
|
|
|
### Primary Colors (Extracted from EPVChat Design)
|
|
|
|
```dart
|
|
class AppColors {
|
|
// Primary Brand Colors
|
|
static const Color primaryBlue = Color(0xFF4A90E2); // Light blue from gradient
|
|
static const Color primaryTeal = Color(0xFF5AC8FA); // Teal/light blue from UI
|
|
static const Color primaryOrange = Color(0xFFFF9500); // Orange from logo
|
|
|
|
// Gradient Colors
|
|
static const Color gradientStart = Color(0xFF4A90E2); // Light blue
|
|
static const Color gradientEnd = Color(0xFF5AC8FA); // Teal
|
|
|
|
// Secondary Colors
|
|
static const Color secondaryBlue = Color(0xFF2E7CD6); // Darker blue
|
|
static const Color accentTeal = Color(0xFF30B0C7); // Deeper teal
|
|
|
|
// Neutral Colors
|
|
static const Color background = Color(0xFFF8F9FA); // Light gray background
|
|
static const Color surface = Color(0xFFFFFFFF); // White surfaces
|
|
static const Color cardBackground = Color(0xFFFFFFFF); // White cards
|
|
|
|
// Text Colors
|
|
static const Color textPrimary = Color(0xFF1A1A1A); // Primary text
|
|
static const Color textSecondary = Color(0xFF6B7280); // Secondary text
|
|
static const Color textHint = Color(0xFF9CA3AF); // Hint text
|
|
|
|
// Status Colors
|
|
static const Color success = Color(0xFF10B981); // Green for success
|
|
static const Color warning = Color(0xFFF59E0B); // Amber for warnings
|
|
static const Color error = Color(0xFFEF4444); // Red for errors
|
|
static const Color info = Color(0xFF3B82F6); // Blue for info
|
|
|
|
// Interactive Colors
|
|
static const Color buttonPrimary = Color(0xFF4A90E2); // Primary button
|
|
static const Color buttonSecondary = Color(0xFFE5E7EB); // Secondary button
|
|
static const Color iconActive = Color(0xFF4A90E2); // Active icons
|
|
static const Color iconInactive = Color(0xFF9CA3AF); // Inactive icons
|
|
|
|
// Chat Specific Colors
|
|
static const Color chatBubbleStudent = Color(0xFF4A90E2); // Student messages
|
|
static const Color chatBubbleAI = Color(0xFFF3F4F6); // AI messages
|
|
static const Color chatInputBackground = Color(0xFFF8F9FA); // Input background
|
|
static const Color chatSendButton = Color(0xFF5AC8FA); // Send button
|
|
}
|
|
```
|
|
|
|
### Color Usage Guidelines
|
|
|
|
#### Primary Colors (70% usage)
|
|
- **Primary Blue**: Main actions, navigation, important CTAs
|
|
- **Primary Teal**: Secondary actions, accents, hover states
|
|
- **Primary Orange**: Highlights, achievements, important notifications
|
|
|
|
#### Neutral Colors (25% usage)
|
|
- **Background**: Page backgrounds, card backgrounds
|
|
- **Surface**: Cards, modals, dropdowns
|
|
- **Text Colors**: Hierarchical text organization
|
|
|
|
#### Status Colors (5% usage)
|
|
- **Success**: Quiz completion, correct answers
|
|
- **Warning**: Low mastery, upcoming deadlines
|
|
- **Error**: Network errors, validation failures
|
|
- **Info**: System notifications, help text
|
|
|
|
---
|
|
|
|
## 📝 TYPOGRAPHY
|
|
|
|
### Font Family
|
|
```dart
|
|
class AppTypography {
|
|
// Primary Font Family (System fonts for consistency)
|
|
static const String primaryFont = 'SF Pro Display'; // iOS
|
|
static const String secondaryFont = 'Roboto'; // Android
|
|
static const String webFont = 'Inter'; // Web
|
|
|
|
// Font Weights
|
|
static const FontWeight thin = FontWeight.w100;
|
|
static const FontWeight light = FontWeight.w300;
|
|
static const FontWeight regular = FontWeight.w400;
|
|
static const FontWeight medium = FontWeight.w500;
|
|
static const FontWeight semiBold = FontWeight.w600;
|
|
static const FontWeight bold = FontWeight.w700;
|
|
static const FontWeight extraBold = FontWeight.w800;
|
|
}
|
|
```
|
|
|
|
### Text Styles
|
|
|
|
```dart
|
|
class AppTextStyles {
|
|
// Headings
|
|
static const TextStyle h1 = TextStyle(
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.textPrimary,
|
|
height: 1.2,
|
|
);
|
|
|
|
static const TextStyle h2 = TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.semiBold,
|
|
color: AppColors.textPrimary,
|
|
height: 1.3,
|
|
);
|
|
|
|
static const TextStyle h3 = TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.semiBold,
|
|
color: AppColors.textPrimary,
|
|
height: 1.4,
|
|
);
|
|
|
|
// Body Text
|
|
static const TextStyle bodyLarge = TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.regular,
|
|
color: AppColors.textPrimary,
|
|
height: 1.5,
|
|
);
|
|
|
|
static const TextStyle bodyMedium = TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.regular,
|
|
color: AppColors.textPrimary,
|
|
height: 1.5,
|
|
);
|
|
|
|
static const TextStyle bodySmall = TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.regular,
|
|
color: AppColors.textSecondary,
|
|
height: 1.4,
|
|
);
|
|
|
|
// Button Text
|
|
static const TextStyle buttonLarge = TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.semiBold,
|
|
color: Colors.white,
|
|
height: 1.2,
|
|
);
|
|
|
|
static const TextStyle buttonMedium = TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.medium,
|
|
color: Colors.white,
|
|
height: 1.2,
|
|
);
|
|
|
|
// Caption and Label
|
|
static const TextStyle caption = TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.regular,
|
|
color: AppColors.textHint,
|
|
height: 1.3,
|
|
);
|
|
|
|
static const TextStyle label = TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.medium,
|
|
color: AppColors.textSecondary,
|
|
height: 1.3,
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 ICONOGRAPHY
|
|
|
|
### Icon Sets
|
|
- **Primary**: Cupertino Icons (iOS-style consistency)
|
|
- **Secondary**: Material Icons (for Android compatibility)
|
|
- **Custom**: Educational icons for specific features
|
|
|
|
### Icon Guidelines
|
|
- **Size**: 24px for standard icons, 16px for compact, 32px for large
|
|
- **Weight**: Medium (consistent across the app)
|
|
- **Color**: Use `iconActive` for active states, `iconInactive` for inactive
|
|
- **Meaning**: Clear, universally understood symbols
|
|
|
|
### Key Icons
|
|
```dart
|
|
class AppIcons {
|
|
// Navigation
|
|
static const IconData home = CupertinoIcons.home;
|
|
static const IconData search = CupertinoIcons.search;
|
|
static const IconData profile = CupertinoIcons.person_circle;
|
|
static const IconData settings = CupertinoIcons.settings;
|
|
static const IconData menu = CupertinoIcons.bars;
|
|
|
|
// Actions
|
|
static const IconData send = CupertinoIcons.paperplane_fill;
|
|
static const IconData add = CupertinoIcons.add;
|
|
static const IconData edit = CupertinoIcons.pencil;
|
|
static const IconData delete = CupertinoIcons.trash;
|
|
static const IconData check = CupertinoIcons.check_mark;
|
|
|
|
// Educational
|
|
static const IconData book = CupertinoIcons.book;
|
|
static const IconData quiz = CupertinoIcons.question_circle;
|
|
static const IconData progress = CupertinoIcons.chart_bar;
|
|
static const IconData lightbulb = CupertinoIcons.lightbulb;
|
|
static const IconData trophy = CupertinoIcons.trophy;
|
|
|
|
// Status
|
|
static const IconData success = CupertinoIcons.check_mark_circled;
|
|
static const IconData warning = CupertinoIcons.exclamationmark_triangle;
|
|
static const IconData error = CupertinoIcons.xmark_circle;
|
|
static const IconData info = CupertinoIcons.info_circle;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📱 COMPONENTS
|
|
|
|
### Buttons
|
|
|
|
#### Primary Button
|
|
```dart
|
|
class PrimaryButton extends StatelessWidget {
|
|
final String text;
|
|
final VoidCallback onPressed;
|
|
final bool isLoading;
|
|
final bool isDisabled;
|
|
|
|
const PrimaryButton({
|
|
Key? key,
|
|
required this.text,
|
|
required this.onPressed,
|
|
this.isLoading = false,
|
|
this.isDisabled = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: isDisabled
|
|
? [AppColors.buttonSecondary, AppColors.buttonSecondary]
|
|
: [AppColors.gradientStart, AppColors.gradientEnd],
|
|
),
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.primaryBlue.withOpacity(0.2),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: isDisabled || isLoading ? null : onPressed,
|
|
child: Center(
|
|
child: isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation(Colors.white),
|
|
),
|
|
)
|
|
: Text(
|
|
text,
|
|
style: AppTextStyles.buttonLarge,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Secondary Button
|
|
```dart
|
|
class SecondaryButton extends StatelessWidget {
|
|
final String text;
|
|
final VoidCallback onPressed;
|
|
final bool isDisabled;
|
|
|
|
const SecondaryButton({
|
|
Key? key,
|
|
required this.text,
|
|
required this.onPressed,
|
|
this.isDisabled = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.buttonSecondary,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: AppColors.primaryBlue.withOpacity(0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: isDisabled ? null : onPressed,
|
|
child: Center(
|
|
child: Text(
|
|
text,
|
|
style: AppTextStyles.buttonLarge.copyWith(
|
|
color: isDisabled
|
|
? AppColors.textHint
|
|
: AppColors.primaryBlue,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Cards
|
|
|
|
#### Standard Card
|
|
```dart
|
|
class StandardCard extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry? padding;
|
|
final EdgeInsetsGeometry? margin;
|
|
final VoidCallback? onTap;
|
|
final bool hasShadow;
|
|
|
|
const StandardCard({
|
|
required this.child,
|
|
this.padding,
|
|
this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 8,
|
|
offset: Offset(0, 2),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 1,
|
|
offset: Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Padding(
|
|
padding: padding ?? EdgeInsets.all(16),
|
|
child: child,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Interactive Card
|
|
```dart
|
|
class InteractiveCard extends StatefulWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry? padding;
|
|
final VoidCallback? onTap;
|
|
final bool isSelected;
|
|
|
|
const InteractiveCard({
|
|
required this.child,
|
|
this.padding,
|
|
this.onTap,
|
|
this.isSelected = false,
|
|
});
|
|
|
|
@override
|
|
State<InteractiveCard> createState() => _InteractiveCardState();
|
|
}
|
|
|
|
class _InteractiveCardState extends State<InteractiveCard> {
|
|
bool isHovered = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MouseRegion(
|
|
onEnter: (_) => setState(() => isHovered = true),
|
|
onExit: (_) => setState(() => isHovered = false),
|
|
child: AnimatedContainer(
|
|
duration: Duration(milliseconds: 200),
|
|
decoration: BoxDecoration(
|
|
color: widget.isSelected
|
|
? AppColors.primaryBlue.withOpacity(0.1)
|
|
: AppColors.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: widget.isSelected
|
|
? Border.all(color: AppColors.primaryBlue, width: 2)
|
|
: null,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(isHovered ? 0.12 : 0.08),
|
|
blurRadius: isHovered ? 12 : 8,
|
|
offset: Offset(0, isHovered ? 4 : 2),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 1,
|
|
offset: Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: widget.onTap,
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Padding(
|
|
padding: widget.padding ?? EdgeInsets.all(16),
|
|
child: widget.child,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Input Fields
|
|
|
|
#### Text Input
|
|
```dart
|
|
class AppTextInput extends StatelessWidget {
|
|
final String? label;
|
|
final String? hint;
|
|
final String? errorText;
|
|
final TextEditingController controller;
|
|
final bool obscureText;
|
|
final TextInputType keyboardType;
|
|
final VoidCallback? onTap;
|
|
final ValueChanged<String>? onChanged;
|
|
final bool isDisabled;
|
|
|
|
const AppTextInput({
|
|
Key? key,
|
|
this.label,
|
|
this.hint,
|
|
this.errorText,
|
|
required this.controller,
|
|
this.obscureText = false,
|
|
this.keyboardType = TextInputType.text,
|
|
this.onTap,
|
|
this.onChanged,
|
|
this.isDisabled = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (label != null) ...[
|
|
Text(
|
|
label!,
|
|
style: AppTextStyles.label,
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: isDisabled ? AppColors.buttonSecondary : AppColors.surface,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: errorText != null
|
|
? AppColors.error
|
|
: AppColors.primaryBlue.withOpacity(0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: TextField(
|
|
controller: controller,
|
|
obscureText: obscureText,
|
|
keyboardType: keyboardType,
|
|
onTap: onTap,
|
|
onChanged: onChanged,
|
|
enabled: !isDisabled,
|
|
decoration: InputDecoration(
|
|
hintText: hint,
|
|
hintStyle: AppTextStyles.bodyMedium.copyWith(
|
|
color: AppColors.textHint,
|
|
),
|
|
border: InputBorder.none,
|
|
contentPadding: const EdgeInsets.all(16),
|
|
),
|
|
style: AppTextStyles.bodyMedium,
|
|
),
|
|
),
|
|
if (errorText != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
errorText!,
|
|
style: AppTextStyles.caption.copyWith(
|
|
color: AppColors.error,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📱 MODERN LAYOUT PATTERNS
|
|
|
|
### Dashboard Layout
|
|
Based on the reference design, implement a modern card-based dashboard with:
|
|
|
|
#### Grid Layout System
|
|
```dart
|
|
class DashboardGrid extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: GridView.count(
|
|
crossAxisCount: 2,
|
|
crossAxisSpacing: 16,
|
|
mainAxisSpacing: 16,
|
|
childAspectRatio: 1.2,
|
|
children: [
|
|
_buildQuickActionCard(
|
|
icon: Icons.chat,
|
|
title: "Ask Tutor",
|
|
subtitle: "Get help with your questions",
|
|
color: AppColors.primaryBlue,
|
|
onTap: () => _navigateToChat(),
|
|
),
|
|
_buildQuickActionCard(
|
|
icon: Icons.quiz,
|
|
title: "Take Quiz",
|
|
subtitle: "Test your knowledge",
|
|
color: AppColors.primaryTeal,
|
|
onTap: () => _navigateToQuiz(),
|
|
),
|
|
_buildQuickActionCard(
|
|
icon: Icons.trending_up,
|
|
title: "Progress",
|
|
subtitle: "Track your learning",
|
|
color: AppColors.primaryOrange,
|
|
onTap: () => _navigateToProgress(),
|
|
),
|
|
_buildQuickActionCard(
|
|
icon: Icons.book,
|
|
title: "Study Materials",
|
|
subtitle: "Access learning resources",
|
|
color: AppColors.secondaryBlue,
|
|
onTap: () => _navigateToMaterials(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickActionCard({
|
|
required IconData icon,
|
|
required String title,
|
|
required String subtitle,
|
|
required Color color,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return InteractiveCard(
|
|
onTap: onTap,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: color,
|
|
size: 24,
|
|
),
|
|
),
|
|
SizedBox(height: 12),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
SizedBox(height: 4),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Content Cards
|
|
```dart
|
|
class ContentCard extends StatelessWidget {
|
|
final String title;
|
|
final String description;
|
|
final String progress;
|
|
final Color accentColor;
|
|
final VoidCallback onTap;
|
|
|
|
const ContentCard({
|
|
required this.title,
|
|
required this.description,
|
|
required this.progress,
|
|
required this.accentColor,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InteractiveCard(
|
|
onTap: onTap,
|
|
padding: EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
description,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: accentColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Icon(
|
|
Icons.arrow_forward,
|
|
color: accentColor,
|
|
size: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 16),
|
|
Container(
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.buttonSecondary,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
child: FractionallySizedBox(
|
|
alignment: Alignment.centerLeft,
|
|
widthFactor: double.parse(progress),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: accentColor,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'${(double.parse(progress) * 100).toInt()}% Complete',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppColors.textHint,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Navigation Bar
|
|
```dart
|
|
class ModernBottomNavigation extends StatelessWidget {
|
|
final int currentIndex;
|
|
final ValueChanged<int> onTap;
|
|
|
|
const ModernBottomNavigation({
|
|
required this.currentIndex,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: SafeArea(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_buildNavItem(
|
|
icon: Icons.home,
|
|
label: "Home",
|
|
index: 0,
|
|
),
|
|
_buildNavItem(
|
|
icon: Icons.chat,
|
|
label: "Chat",
|
|
index: 1,
|
|
),
|
|
_buildNavItem(
|
|
icon: Icons.quiz,
|
|
label: "Quiz",
|
|
index: 2,
|
|
),
|
|
_buildNavItem(
|
|
icon: Icons.person,
|
|
label: "Profile",
|
|
index: 3,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNavItem({
|
|
required IconData icon,
|
|
required String label,
|
|
required int index,
|
|
}) {
|
|
final isActive = currentIndex == index;
|
|
final color = isActive ? AppColors.primaryBlue : AppColors.iconInactive;
|
|
|
|
return GestureDetector(
|
|
onTap: () => onTap(index),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: isActive ? AppColors.primaryBlue.withOpacity(0.1) : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: color,
|
|
size: 24,
|
|
),
|
|
SizedBox(height: 4),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 12,
|
|
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Chat Interface
|
|
```dart
|
|
class ModernChatInterface extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
// Chat Header
|
|
Container(
|
|
padding: EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: AppColors.buttonSecondary,
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
backgroundColor: AppColors.primaryBlue.withOpacity(0.1),
|
|
child: Icon(
|
|
Icons.smart_toy,
|
|
color: AppColors.primaryBlue,
|
|
),
|
|
),
|
|
SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"AI Tutor",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
Text(
|
|
"Always here to help",
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
Icons.more_vert,
|
|
color: AppColors.iconInactive,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Chat Messages
|
|
Expanded(
|
|
child: Container(
|
|
color: AppColors.background,
|
|
child: ListView.builder(
|
|
padding: EdgeInsets.all(16),
|
|
itemCount: messages.length,
|
|
itemBuilder: (context, index) {
|
|
final message = messages[index];
|
|
return _buildMessageBubble(message);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
// Chat Input
|
|
Container(
|
|
padding: EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
border: Border(
|
|
top: BorderSide(
|
|
color: AppColors.buttonSecondary,
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.chatInputBackground,
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(
|
|
color: AppColors.primaryBlue.withOpacity(0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: TextField(
|
|
decoration: InputDecoration(
|
|
hintText: "Ask your question...",
|
|
hintStyle: TextStyle(
|
|
color: AppColors.textHint,
|
|
),
|
|
border: InputBorder.none,
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 12),
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [AppColors.primaryBlue, AppColors.primaryTeal],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
child: Icon(
|
|
Icons.send,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMessageBubble(Message message) {
|
|
final isUser = message.isUser;
|
|
return Container(
|
|
margin: EdgeInsets.only(bottom: 16),
|
|
child: Row(
|
|
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
|
children: [
|
|
if (!isUser) ...[
|
|
CircleAvatar(
|
|
radius: 16,
|
|
backgroundColor: AppColors.primaryBlue.withOpacity(0.1),
|
|
child: Icon(
|
|
Icons.smart_toy,
|
|
color: AppColors.primaryBlue,
|
|
size: 16,
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
],
|
|
Flexible(
|
|
child: Container(
|
|
constraints: BoxConstraints(
|
|
maxWidth: MediaQuery.of(context).size.width * 0.7,
|
|
),
|
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: isUser ? AppColors.primaryBlue : AppColors.chatBubbleAI,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Text(
|
|
message.text,
|
|
style: TextStyle(
|
|
color: isUser ? Colors.white : AppColors.textPrimary,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (isUser) ...[
|
|
SizedBox(width: 8),
|
|
CircleAvatar(
|
|
radius: 16,
|
|
backgroundColor: AppColors.primaryOrange.withOpacity(0.1),
|
|
child: Icon(
|
|
Icons.person,
|
|
color: AppColors.primaryOrange,
|
|
size: 16,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎭 ANIMATIONS
|
|
|
|
### Animation Principles
|
|
- **Purposeful**: Every animation should enhance user understanding
|
|
- **Smooth**: Use easing functions that feel natural
|
|
- **Fast**: Respect user time, keep animations under 300ms
|
|
- **Consistent**: Use similar timing across the app
|
|
|
|
### Standard Animation Durations
|
|
```dart
|
|
class AppAnimations {
|
|
static const Duration fast = Duration(milliseconds: 150);
|
|
static const Duration medium = Duration(milliseconds: 250);
|
|
static const Duration slow = Duration(milliseconds: 350);
|
|
static const Duration extraSlow = Duration(milliseconds: 500);
|
|
}
|
|
```
|
|
|
|
### Curves
|
|
```dart
|
|
class AppCurves {
|
|
static const Curve easeIn = Curves.easeIn;
|
|
static const Curve easeOut = Curves.easeOut;
|
|
static const Curve easeInOut = Curves.easeInOut;
|
|
static const Curve bounceIn = Curves.bounceIn;
|
|
static const Curve elasticOut = Curves.elasticOut;
|
|
}
|
|
```
|
|
|
|
### Common Animations
|
|
|
|
#### Fade Transition
|
|
```dart
|
|
class FadeIn extends StatefulWidget {
|
|
final Widget child;
|
|
final Duration duration;
|
|
|
|
const FadeIn({
|
|
Key? key,
|
|
required this.child,
|
|
this.duration = AppAnimations.medium,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<FadeIn> createState() => _FadeInState();
|
|
}
|
|
|
|
class _FadeInState extends State<FadeIn>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: widget.duration,
|
|
vsync: this,
|
|
);
|
|
_animation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _controller,
|
|
curve: AppCurves.easeIn,
|
|
));
|
|
|
|
_controller.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FadeTransition(
|
|
opacity: _animation,
|
|
child: widget.child,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Slide Transition
|
|
```dart
|
|
class SlideIn extends StatefulWidget {
|
|
final Widget child;
|
|
final Duration duration;
|
|
final Offset begin;
|
|
final Offset end;
|
|
|
|
const SlideIn({
|
|
Key? key,
|
|
required this.child,
|
|
this.duration = AppAnimations.medium,
|
|
this.begin = const Offset(0, 0.3),
|
|
this.end = Offset.zero,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<SlideIn> createState() => _SlideInState();
|
|
}
|
|
|
|
class _SlideInState extends State<SlideIn>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<Offset> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: widget.duration,
|
|
vsync: this,
|
|
);
|
|
_animation = Tween<Offset>(
|
|
begin: widget.begin,
|
|
end: widget.end,
|
|
).animate(CurvedAnimation(
|
|
parent: _controller,
|
|
curve: AppCurves.easeOut,
|
|
));
|
|
|
|
_controller.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SlideTransition(
|
|
position: _animation,
|
|
child: widget.child,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Scale Animation
|
|
```dart
|
|
class ScaleIn extends StatefulWidget {
|
|
final Widget child;
|
|
final Duration duration;
|
|
|
|
const ScaleIn({
|
|
Key? key,
|
|
required this.child,
|
|
this.duration = AppAnimations.medium,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<ScaleIn> createState() => _ScaleInState();
|
|
}
|
|
|
|
class _ScaleInState extends State<ScaleIn>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: widget.duration,
|
|
vsync: this,
|
|
);
|
|
_animation = Tween<double>(
|
|
begin: 0.8,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _controller,
|
|
curve: AppCurves.elasticOut,
|
|
));
|
|
|
|
_controller.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ScaleTransition(
|
|
scale: _animation,
|
|
child: widget.child,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📐 LAYOUT GUIDELINES
|
|
|
|
### Spacing System
|
|
```dart
|
|
class AppSpacing {
|
|
// Base spacing unit (8px)
|
|
static const double xs = 4; // 0.5x
|
|
static const double sm = 8; // 1x
|
|
static const double md = 16; // 2x
|
|
static const double lg = 24; // 3x
|
|
static const double xl = 32; // 4x
|
|
static const double xxl = 48; // 6x
|
|
static const double xxxl = 64; // 8x
|
|
}
|
|
```
|
|
|
|
### Border Radius
|
|
```dart
|
|
class AppBorderRadius {
|
|
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 round = 1000; // For circles
|
|
}
|
|
```
|
|
|
|
### Screen Breakpoints
|
|
```dart
|
|
class AppBreakpoints {
|
|
static const double mobile = 480;
|
|
static const double tablet = 768;
|
|
static const double desktop = 1024;
|
|
static const double largeDesktop = 1440;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 SCREEN-SPECIFIC DESIGNS
|
|
|
|
### Login Screen
|
|
- **Background**: Gradient from `gradientStart` to `gradientEnd`
|
|
- **Logo**: Centered with fade-in animation
|
|
- **Form**: White card with subtle shadow
|
|
- **Buttons**: Primary gradient button for login, secondary for signup
|
|
|
|
### Student Dashboard
|
|
- **Header**: App bar with gradient background
|
|
- **Cards**: Grid layout with mastery progress bars
|
|
- **Navigation**: Bottom navigation with active state indicators
|
|
- **Animations**: Cards slide in from bottom on load
|
|
|
|
### Chat Interface
|
|
- **Background**: Light gray (`chatInputBackground`)
|
|
- **Messages**: Different colors for student vs AI
|
|
- **Input**: Rounded input field with send button
|
|
- **Animations**: Messages slide in from sides
|
|
|
|
### Quiz Screen
|
|
- **Timer**: Circular progress indicator
|
|
- **Questions**: Card-based layout
|
|
- **Options**: Radio buttons with hover states
|
|
- **Progress**: Linear progress bar at top
|
|
|
|
---
|
|
|
|
## 🌙 DARK MODE
|
|
|
|
### Dark Color Palette
|
|
```dart
|
|
class AppDarkColors {
|
|
// Background Colors
|
|
static const Color background = Color(0xFF121212);
|
|
static const Color surface = Color(0xFF1E1E1E);
|
|
static const Color cardBackground = Color(0xFF2D2D2D);
|
|
|
|
// Text Colors
|
|
static const Color textPrimary = Color(0xFFFFFFFF);
|
|
static const Color textSecondary = Color(0xFFB3B3B3);
|
|
static const Color textHint = Color(0xFF808080);
|
|
|
|
// Keep primary colors the same
|
|
static const Color primaryBlue = AppColors.primaryBlue;
|
|
static const Color primaryTeal = AppColors.primaryTeal;
|
|
static const Color primaryOrange = AppColors.primaryOrange;
|
|
}
|
|
```
|
|
|
|
### Implementation Guidelines
|
|
- **Automatic**: Follow system dark mode preference
|
|
- **Manual**: Toggle in app settings
|
|
- **Consistency**: All screens must support both themes
|
|
- **Animation**: Smooth transition between themes
|
|
|
|
---
|
|
|
|
## 📱 RESPONSIVE DESIGN
|
|
|
|
### Mobile (< 768px)
|
|
- **Layout**: Single column, scrollable
|
|
- **Navigation**: Bottom navigation bar
|
|
- **Cards**: Full width with horizontal padding
|
|
- **Typography**: Base sizes as defined
|
|
|
|
### Tablet (768px - 1024px)
|
|
- **Layout**: Two-column where appropriate
|
|
- **Navigation**: Side drawer or top navigation
|
|
- **Cards**: Grid layout (2-3 columns)
|
|
- **Typography**: Slightly larger base sizes
|
|
|
|
### Desktop (> 1024px)
|
|
- **Layout**: Multi-column, fixed width centered
|
|
- **Navigation**: Top navigation bar
|
|
- **Cards**: Grid layout (3-4 columns)
|
|
- **Typography**: Larger base sizes, better readability
|
|
|
|
---
|
|
|
|
## 🎯 ACCESSIBILITY
|
|
|
|
### Color Contrast
|
|
- **Normal Text**: Minimum 4.5:1 contrast ratio
|
|
- **Large Text**: Minimum 3:1 contrast ratio
|
|
- **Interactive Elements**: Minimum 3:1 contrast ratio
|
|
|
|
### Typography
|
|
- **Minimum Size**: 14px for body text
|
|
- **Line Height**: 1.5 for better readability
|
|
- **Font Weight**: Regular or medium for body text
|
|
|
|
### Focus States
|
|
- **Visible**: All interactive elements have visible focus states
|
|
- **Consistent**: Same focus style across the app
|
|
- **Keyboard**: Full keyboard navigation support
|
|
|
|
### Screen Reader Support
|
|
- **Labels**: All images and icons have alt text
|
|
- **Semantic**: Use semantic HTML elements
|
|
- **Announcements**: Important changes are announced
|
|
|
|
---
|
|
|
|
## 🚀 PERFORMANCE
|
|
|
|
### Animation Performance
|
|
- **60 FPS**: All animations should maintain 60 FPS
|
|
- **GPU**: Use GPU-accelerated animations where possible
|
|
- **Simplify**: Avoid complex animations on low-end devices
|
|
|
|
### Asset Optimization
|
|
- **Images**: WebP format with appropriate sizes
|
|
- **Icons**: SVG icons where possible
|
|
- **Fonts**: Subset fonts to reduce size
|
|
|
|
### Bundle Size
|
|
- **Tree Shaking**: Remove unused code
|
|
- **Lazy Loading**: Load features on demand
|
|
- **Compression**: Compress all assets
|
|
|
|
---
|
|
|
|
## 📋 DESIGN CHECKLIST
|
|
|
|
### Before Implementation
|
|
- [ ] Color palette defined and consistent
|
|
- [ ] Typography scale established
|
|
- [ ] Component library created
|
|
- [ ] Animation system implemented
|
|
- [ ] Accessibility guidelines reviewed
|
|
|
|
### During Implementation
|
|
- [ ] Components follow design system
|
|
- [ ] Animations are smooth and purposeful
|
|
- [ ] Responsive design works on all breakpoints
|
|
- [ ] Dark mode is supported
|
|
- [ ] Accessibility features are implemented
|
|
|
|
### Before Release
|
|
- [ ] All screens reviewed for consistency
|
|
- [ ] Performance tested on target devices
|
|
- [ ] Accessibility tested with screen readers
|
|
- [ ] User testing completed
|
|
- [ ] Design documentation updated
|
|
|
|
---
|
|
|
|
## 🔄 DESIGN SYSTEM MAINTENANCE
|
|
|
|
### Regular Updates
|
|
- **Monthly**: Review usage analytics and user feedback
|
|
- **Quarterly**: Update components and add new ones
|
|
- **Annually**: Major design system updates
|
|
|
|
### Version Control
|
|
- **Semantic Versioning**: Use semantic versioning for releases
|
|
- **Changelog**: Maintain detailed changelog
|
|
- **Migration**: Provide migration guides for breaking changes
|
|
|
|
### Documentation
|
|
- **Living Document**: Keep documentation up to date
|
|
- **Examples**: Provide code examples for all components
|
|
- **Guidelines**: Maintain usage guidelines and best practices
|
|
|
|
---
|
|
|
|
*Last Updated: 2026-05-06*
|
|
*Version: 1.0.0*
|
|
*Design System Owner: UI/UX Team*
|