feat(ui): update abstract background to smooth gradient, adapt stat cards sizes and platform layout, fix search bar clear button glitch
This commit is contained in:
1 parent
37d2d5b9a7
commit
1fc2242672
4 files changed
+166
-155
No files matched your search
@@ -677,6 +677,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final isWindowsApp = Theme.of(context).platform == TargetPlatform.windows || constraints.maxWidth >= 1000;
|
||||
|
||||
int crossAxisCount;
|
||||
if (constraints.maxWidth >= 1000) {
|
||||
crossAxisCount = isAdmin ? 4 : 3;
|
||||
@@ -685,6 +687,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
} else {
|
||||
crossAxisCount = 1;
|
||||
}
|
||||
|
||||
final isSquare = isWindowsApp && crossAxisCount > 1;
|
||||
|
||||
return GridView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -692,13 +697,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
mainAxisExtent: crossAxisCount == 1 ? 110 : 130,
|
||||
mainAxisExtent: isSquare ? 115 : (crossAxisCount == 1 ? 85 : 95),
|
||||
),
|
||||
children: [
|
||||
StatCard(
|
||||
title: 'Total Students',
|
||||
value: stats.totalStudents.toString(),
|
||||
icon: Icons.school_rounded,
|
||||
isSquare: isSquare,
|
||||
onTap: () {
|
||||
ref.read(studentQueryProvider.notifier).reset();
|
||||
ref.invalidate(studentPageProvider);
|
||||
@@ -711,6 +717,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
value: stats.activeUsers.toString(),
|
||||
icon: Icons.group,
|
||||
iconColor: Colors.blue,
|
||||
isSquare: isSquare,
|
||||
onTap: () {
|
||||
ref.invalidate(usersProvider);
|
||||
ref.read(activeTabProvider.notifier).setTab('Users');
|
||||
@@ -721,6 +728,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
value: stats.completedDocuments.toString(),
|
||||
icon: Icons.fact_check_rounded,
|
||||
iconColor: AppColors.primaryGreen,
|
||||
isSquare: isSquare,
|
||||
onTap: () {
|
||||
ref.invalidate(foldersProvider);
|
||||
ref.invalidate(documentPageProvider);
|
||||
@@ -732,6 +740,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
value: stats.missingDocuments.toString(),
|
||||
icon: Icons.assignment_late_rounded,
|
||||
iconColor: Colors.orange,
|
||||
isSquare: isSquare,
|
||||
onTap: () {
|
||||
ref.invalidate(foldersProvider);
|
||||
ref.invalidate(documentPageProvider);
|
||||
|
||||
@@ -7,6 +7,7 @@ class StatCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color? iconColor;
|
||||
final VoidCallback? onTap;
|
||||
final bool isSquare;
|
||||
|
||||
const StatCard({
|
||||
super.key,
|
||||
@@ -16,6 +17,7 @@ class StatCard extends StatelessWidget {
|
||||
required this.icon,
|
||||
this.iconColor,
|
||||
this.onTap,
|
||||
this.isSquare = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -23,11 +25,10 @@ class StatCard extends StatelessWidget {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isSmall = constraints.maxWidth < 220;
|
||||
final iconSize = isSmall ? 34.0 : 36.0;
|
||||
final titleFontSize = isSmall ? 12.0 : 14.0;
|
||||
final valueFontSize = isSmall ? 22.0 : 28.0;
|
||||
final padding = isSmall ? 12.0 : 20.0;
|
||||
final iconPadding = isSmall ? 8.0 : 12.0;
|
||||
final iconSize = isSmall ? 24.0 : 28.0;
|
||||
final titleFontSize = isSmall ? 11.0 : 13.0;
|
||||
final valueFontSize = isSmall ? 20.0 : 24.0;
|
||||
final padding = isSmall ? 10.0 : 14.0;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -35,9 +36,9 @@ class StatCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -49,73 +50,146 @@ class StatCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon Container
|
||||
Padding(
|
||||
padding: EdgeInsets.all(iconPadding),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: iconColor ?? const Color(0xFF1C8248),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Text Details
|
||||
Expanded(
|
||||
child: Column(
|
||||
child: isSquare
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: (iconColor ?? const Color(0xFF1C8248)).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: iconColor ?? const Color(0xFF1C8248),
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: isSmall ? 9.0 : 11.0,
|
||||
color: Colors.grey.shade500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
// Icon Container
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: (iconColor ?? const Color(0xFF1C8248)).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: iconColor ?? const Color(0xFF1C8248),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// FittedBox shrinks the text dynamically if the number gets too massive
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Text Details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: valueFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: isSmall ? 9.0 : 11.0,
|
||||
color: Colors.grey.shade500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: isSmall ? 10.0 : 12.0,
|
||||
color: Colors.grey.shade500,
|
||||
fontWeight: FontWeight.w500,
|
||||
if (onTap != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -262,13 +262,8 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
||||
onPressed: () {
|
||||
_controller.clear();
|
||||
widget.onChanged?.call('');
|
||||
if (widget.collapsible) {
|
||||
_focusNode.unfocus();
|
||||
_removeOverlay();
|
||||
setState(() => _isExpanded = false);
|
||||
} else {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
widget.onSubmitted?.call('');
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
|
||||
/// A lightweight, static abstract background decorator featuring exactly 3 large, solid shapes (no outlines).
|
||||
/// A lightweight, static abstract background decorator featuring a smooth, modern gradient background.
|
||||
/// Fully responsive across Mobile, Web, and Desktop platforms.
|
||||
class AbstractBackground extends StatelessWidget {
|
||||
final Widget child;
|
||||
@@ -14,87 +13,21 @@ class AbstractBackground extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Static abstract background shapes layer (Ignored for pointer events)
|
||||
const IgnorePointer(
|
||||
child: _AbstractShapesCanvas(),
|
||||
),
|
||||
// Screen Content Layer
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AbstractShapesCanvas extends StatelessWidget {
|
||||
const _AbstractShapesCanvas();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final height = constraints.maxHeight;
|
||||
|
||||
if (width == 0 || height == 0) return const SizedBox.shrink();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Base background tint
|
||||
Container(color: AppColors.pageBackground),
|
||||
|
||||
// Shape 1 (Top-Right): Large Solid Circle
|
||||
Positioned(
|
||||
top: -height * 0.12,
|
||||
right: -width * 0.10,
|
||||
child: Container(
|
||||
width: math.max(320, width * 0.55),
|
||||
height: math.max(320, width * 0.55),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.075),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Shape 2 (Top-Left): Large Solid Tilted Rect Block
|
||||
Positioned(
|
||||
top: height * 0.08,
|
||||
left: -width * 0.12,
|
||||
child: Transform.rotate(
|
||||
angle: -math.pi / 6,
|
||||
child: Container(
|
||||
width: math.max(260, width * 0.45),
|
||||
height: math.max(140, width * 0.22),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
color: AppColors.darkGreen.withValues(alpha: 0.055),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Shape 3 (Bottom-Right): Large Solid Tilted Pill Capsule
|
||||
Positioned(
|
||||
bottom: height * 0.05,
|
||||
right: -width * 0.06,
|
||||
child: Transform.rotate(
|
||||
angle: math.pi / 5,
|
||||
child: Container(
|
||||
width: math.max(280, width * 0.50),
|
||||
height: math.max(120, width * 0.22),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.065),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
const Color(0xFFEAF3ED),
|
||||
AppColors.pageBackground,
|
||||
const Color(0xFFE4F0E8),
|
||||
AppColors.pageBackground,
|
||||
],
|
||||
);
|
||||
},
|
||||
stops: const [0.0, 0.35, 0.7, 1.0],
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user