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:
ssbg04 committed 2026-07-22 15:47:51 +08:00
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);
+139 -65
View File
@@ -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,
);
}
}