From 7fb465f6c90cf15f5f8f9dee815ac322795f0b65 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Tue, 17 Feb 2026 07:27:42 +0800 Subject: [PATCH] Created App Surfaces for Theming --- lib/screens/admin/offices_screen.dart | 2 + lib/screens/admin/user_management_screen.dart | 8 +- lib/screens/dashboard/dashboard_screen.dart | 3 +- .../notifications/notifications_screen.dart | 5 + .../searchable_multi_select_dropdown.dart | 3 + .../shared/under_development_screen.dart | 5 +- lib/screens/tasks/task_detail_screen.dart | 21 ++- lib/screens/tasks/tasks_list_screen.dart | 8 +- lib/screens/teams/teams_screen.dart | 3 + lib/screens/tickets/ticket_detail_screen.dart | 30 +++- lib/screens/tickets/tickets_list_screen.dart | 4 + lib/screens/workforce/workforce_screen.dart | 7 +- lib/theme/app_surfaces.dart | 90 ++++++++++++ lib/theme/app_theme.dart | 23 +++- lib/widgets/task_assignment_section.dart | 3 + lib/widgets/tasq_adaptive_list.dart | 15 +- test/theme_overhaul_test.dart | 128 ++++++++++++++++++ 17 files changed, 336 insertions(+), 22 deletions(-) create mode 100644 lib/theme/app_surfaces.dart create mode 100644 test/theme_overhaul_test.dart diff --git a/lib/screens/admin/offices_screen.dart b/lib/screens/admin/offices_screen.dart index bf63335f..d701330d 100644 --- a/lib/screens/admin/offices_screen.dart +++ b/lib/screens/admin/offices_screen.dart @@ -7,6 +7,7 @@ import '../../providers/profile_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; +import '../../theme/app_surfaces.dart'; import '../../widgets/tasq_adaptive_list.dart'; class OfficesScreen extends ConsumerStatefulWidget { @@ -177,6 +178,7 @@ class _OfficesScreenState extends ConsumerState { context: context, builder: (dialogContext) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: Text(office == null ? 'Create Office' : 'Edit Office'), content: TextField( controller: nameController, diff --git a/lib/screens/admin/user_management_screen.dart b/lib/screens/admin/user_management_screen.dart index f0da2cec..169b09d6 100644 --- a/lib/screens/admin/user_management_screen.dart +++ b/lib/screens/admin/user_management_screen.dart @@ -8,6 +8,7 @@ import '../../models/user_office.dart'; import '../../providers/admin_user_provider.dart'; import '../../providers/profile_provider.dart'; import '../../providers/tickets_provider.dart'; +import '../../theme/app_surfaces.dart'; import '../../providers/user_offices_provider.dart'; import '../../utils/app_time.dart'; import '../../widgets/mono_text.dart'; @@ -241,8 +242,10 @@ class _UserManagementScreenState extends ConsumerState { }, onRequestRefresh: () { // For server-side pagination, update the query provider - ref.read(adminUserQueryProvider.notifier).state = - const AdminUserQuery(offset: 0, limit: 50); + ref.read(adminUserQueryProvider.notifier).state = const AdminUserQuery( + offset: 0, + limit: 50, + ); }, isLoading: false, ); @@ -289,6 +292,7 @@ class _UserManagementScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Update user'), content: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index e7d7137d..6332365e 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -10,6 +10,7 @@ import '../../providers/profile_provider.dart'; import '../../providers/tasks_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../widgets/responsive_body.dart'; +import '../../theme/app_surfaces.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/status_pill.dart'; import '../../utils/app_time.dart'; @@ -497,7 +498,7 @@ class _StaffTable extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(AppSurfaces.of(context).cardRadius), border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), ), child: Column( diff --git a/lib/screens/notifications/notifications_screen.dart b/lib/screens/notifications/notifications_screen.dart index c48ab543..72cab924 100644 --- a/lib/screens/notifications/notifications_screen.dart +++ b/lib/screens/notifications/notifications_screen.dart @@ -8,6 +8,7 @@ import '../../providers/tasks_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; +import '../../theme/app_surfaces.dart'; class NotificationsScreen extends ConsumerWidget { const NotificationsScreen({super.key}); @@ -74,7 +75,11 @@ class NotificationsScreen extends ConsumerWidget { final title = _notificationTitle(item.type, actorName); final icon = _notificationIcon(item.type); + // Use a slightly more compact card for dense notification lists + // — 12px radius, subtle shadow so the list remains readable. return Card( + shape: AppSurfaces.of(context).compactShape, + shadowColor: AppSurfaces.of(context).compactShadowColor, child: ListTile( leading: Icon(icon), title: Text(title), diff --git a/lib/screens/searchable_multi_select_dropdown.dart b/lib/screens/searchable_multi_select_dropdown.dart index 9c156862..f4938994 100644 --- a/lib/screens/searchable_multi_select_dropdown.dart +++ b/lib/screens/searchable_multi_select_dropdown.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../theme/app_surfaces.dart'; + /// Searchable multi-select dropdown with chips and 'Select All' option class SearchableMultiSelectDropdown extends StatefulWidget { const SearchableMultiSelectDropdown({ @@ -53,6 +55,7 @@ class SearchableMultiSelectDropdownState ) .toList(); return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: Text('Select ${widget.label}'), content: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/shared/under_development_screen.dart b/lib/screens/shared/under_development_screen.dart index c897cbc9..b9542c01 100644 --- a/lib/screens/shared/under_development_screen.dart +++ b/lib/screens/shared/under_development_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../theme/app_surfaces.dart'; import '../../widgets/responsive_body.dart'; class UnderDevelopmentScreen extends StatelessWidget { @@ -33,7 +34,9 @@ class UnderDevelopmentScreen extends StatelessWidget { color: Theme.of( context, ).colorScheme.primaryContainer.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).dialogRadius, + ), ), child: Icon( icon, diff --git a/lib/screens/tasks/task_detail_screen.dart b/lib/screens/tasks/task_detail_screen.dart index 89390fd2..bd7cd78a 100644 --- a/lib/screens/tasks/task_detail_screen.dart +++ b/lib/screens/tasks/task_detail_screen.dart @@ -16,6 +16,7 @@ import '../../widgets/app_breakpoints.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../widgets/status_pill.dart'; +import '../../theme/app_surfaces.dart'; import '../../widgets/task_assignment_section.dart'; import '../../widgets/typing_dots.dart'; @@ -377,10 +378,18 @@ class _TaskDetailScreenState extends ConsumerState { decoration: BoxDecoration( color: bubbleColor, borderRadius: BorderRadius.only( - topLeft: const Radius.circular(16), - topRight: const Radius.circular(16), - bottomLeft: Radius.circular(isMe ? 16 : 4), - bottomRight: Radius.circular(isMe ? 4 : 16), + topLeft: Radius.circular( + AppSurfaces.of(context).cardRadius, + ), + topRight: Radius.circular( + AppSurfaces.of(context).cardRadius, + ), + bottomLeft: Radius.circular( + isMe ? AppSurfaces.of(context).cardRadius : 4, + ), + bottomRight: Radius.circular( + isMe ? 4 : AppSurfaces.of(context).cardRadius, + ), ), ), child: _buildMentionText(message.content, textColor, profiles), @@ -695,7 +704,9 @@ class _TaskDetailScreenState extends ConsumerState { constraints: const BoxConstraints(maxHeight: 200), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).compactCardRadius, + ), ), child: ListView.separated( shrinkWrap: true, diff --git a/lib/screens/tasks/tasks_list_screen.dart b/lib/screens/tasks/tasks_list_screen.dart index 84e690da..07839e06 100644 --- a/lib/screens/tasks/tasks_list_screen.dart +++ b/lib/screens/tasks/tasks_list_screen.dart @@ -17,6 +17,7 @@ import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/typing_dots.dart'; +import '../../theme/app_surfaces.dart'; class TasksListScreen extends ConsumerStatefulWidget { const TasksListScreen({super.key}); @@ -385,6 +386,7 @@ class _TasksListScreenState extends ConsumerState { builder: (context, setState) { final officesAsync = ref.watch(officesProvider); return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Create Task'), content: SizedBox( width: 360, @@ -499,7 +501,9 @@ class _TasksListScreenState extends ConsumerState { alignment: Alignment.center, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).compactCardRadius, + ), ), child: Text( '#$queueOrder', @@ -691,6 +695,8 @@ class _StatusSummaryCard extends StatelessWidget { return Card( color: background, + // summary cards are compact — use compact token for consistent density + shape: AppSurfaces.of(context).compactShape, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( diff --git a/lib/screens/teams/teams_screen.dart b/lib/screens/teams/teams_screen.dart index 8b770275..14746f28 100644 --- a/lib/screens/teams/teams_screen.dart +++ b/lib/screens/teams/teams_screen.dart @@ -7,6 +7,7 @@ import '../../providers/teams_provider.dart'; import '../../providers/profile_provider.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tasq/screens/searchable_multi_select_dropdown.dart'; +import '../../theme/app_surfaces.dart'; import '../../widgets/tasq_adaptive_list.dart'; final officesProvider = FutureProvider>((ref) async { @@ -150,6 +151,7 @@ class TeamsScreen extends ConsumerWidget { return StatefulBuilder( builder: (context, setState) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: Text(isEdit ? 'Edit Team' : 'Add Team'), content: SingleChildScrollView( child: Column( @@ -266,6 +268,7 @@ class TeamsScreen extends ConsumerWidget { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Delete Team'), content: const Text('Are you sure you want to delete this team?'), actions: [ diff --git a/lib/screens/tickets/ticket_detail_screen.dart b/lib/screens/tickets/ticket_detail_screen.dart index 5835f1a0..0a36a09d 100644 --- a/lib/screens/tickets/ticket_detail_screen.dart +++ b/lib/screens/tickets/ticket_detail_screen.dart @@ -19,6 +19,7 @@ import '../../widgets/responsive_body.dart'; import '../../widgets/status_pill.dart'; import '../../widgets/task_assignment_section.dart'; import '../../widgets/typing_dots.dart'; +import '../../theme/app_surfaces.dart'; class TicketDetailScreen extends ConsumerStatefulWidget { const TicketDetailScreen({super.key, required this.ticketId}); @@ -233,13 +234,25 @@ class _TicketDetailScreenState extends ConsumerState { decoration: BoxDecoration( color: bubbleColor, borderRadius: BorderRadius.only( - topLeft: const Radius.circular(16), - topRight: const Radius.circular(16), + topLeft: Radius.circular( + AppSurfaces.of(context).cardRadius, + ), + topRight: Radius.circular( + AppSurfaces.of(context).cardRadius, + ), bottomLeft: Radius.circular( - isMe ? 16 : 4, + isMe + ? AppSurfaces.of( + context, + ).cardRadius + : 4, ), bottomRight: Radius.circular( - isMe ? 4 : 16, + isMe + ? 4 + : AppSurfaces.of( + context, + ).cardRadius, ), ), ), @@ -609,7 +622,9 @@ class _TicketDetailScreenState extends ConsumerState { constraints: const BoxConstraints(maxHeight: 200), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).compactCardRadius, + ), ), child: ListView.separated( shrinkWrap: true, @@ -811,6 +826,7 @@ class _TicketDetailScreenState extends ConsumerState { context: context, builder: (dialogContext) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Ticket Timeline'), content: Column( mainAxisSize: MainAxisSize.min, @@ -929,7 +945,9 @@ class _MetaBadge extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: background, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).compactCardRadius, + ), border: Border.all(color: border), ), child: Row( diff --git a/lib/screens/tickets/tickets_list_screen.dart b/lib/screens/tickets/tickets_list_screen.dart index 889a3411..46a90308 100644 --- a/lib/screens/tickets/tickets_list_screen.dart +++ b/lib/screens/tickets/tickets_list_screen.dart @@ -15,6 +15,7 @@ import '../../widgets/mono_text.dart'; import '../../widgets/responsive_body.dart'; import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/typing_dots.dart'; +import '../../theme/app_surfaces.dart'; class TicketsListScreen extends ConsumerStatefulWidget { const TicketsListScreen({super.key}); @@ -321,6 +322,7 @@ class _TicketsListScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setState) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Create Ticket'), content: Consumer( builder: (context, ref, child) { @@ -582,6 +584,8 @@ class _StatusSummaryCard extends StatelessWidget { return Card( color: background, + // summary cards are compact — use compact token for consistent density + shape: AppSurfaces.of(context).compactShape, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( diff --git a/lib/screens/workforce/workforce_screen.dart b/lib/screens/workforce/workforce_screen.dart index 9295c7e9..ee9c4130 100644 --- a/lib/screens/workforce/workforce_screen.dart +++ b/lib/screens/workforce/workforce_screen.dart @@ -12,6 +12,7 @@ import '../../providers/profile_provider.dart'; import '../../providers/workforce_provider.dart'; import '../../utils/app_time.dart'; import '../../widgets/responsive_body.dart'; +import '../../theme/app_surfaces.dart'; class WorkforceScreen extends ConsumerWidget { const WorkforceScreen({super.key}); @@ -477,6 +478,7 @@ class _ScheduleTile extends ConsumerWidget { context: context, builder: (dialogContext) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Request swap'), content: DropdownButtonFormField( initialValue: selectedId, @@ -560,6 +562,7 @@ class _ScheduleTile extends ConsumerWidget { completer.complete(dialogContext); } return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Validating location'), content: Row( children: [ @@ -894,7 +897,9 @@ class _ScheduleGeneratorPanelState padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular( + AppSurfaces.of(context).compactCardRadius, + ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/theme/app_surfaces.dart b/lib/theme/app_surfaces.dart new file mode 100644 index 00000000..cd2b203e --- /dev/null +++ b/lib/theme/app_surfaces.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +@immutable +class AppSurfaces extends ThemeExtension { + const AppSurfaces({ + required this.cardRadius, + required this.compactCardRadius, + required this.dialogRadius, + required this.cardElevation, + required this.cardShadowColor, + required this.compactShadowColor, + }); + + final double cardRadius; + final double compactCardRadius; + final double dialogRadius; + final double cardElevation; + final Color cardShadowColor; + final Color compactShadowColor; + + // convenience shapes + RoundedRectangleBorder get standardShape => + RoundedRectangleBorder(borderRadius: BorderRadius.circular(cardRadius)); + RoundedRectangleBorder get compactShape => RoundedRectangleBorder( + borderRadius: BorderRadius.circular(compactCardRadius), + ); + RoundedRectangleBorder get dialogShape => + RoundedRectangleBorder(borderRadius: BorderRadius.circular(dialogRadius)); + + static AppSurfaces of(BuildContext context) { + final ext = Theme.of(context).extension(); + return ext ?? + const AppSurfaces( + cardRadius: 16, + compactCardRadius: 12, + dialogRadius: 20, + cardElevation: 3, + cardShadowColor: Color.fromRGBO(0, 0, 0, 0.12), + compactShadowColor: Color.fromRGBO(0, 0, 0, 0.08), + ); + } + + @override + AppSurfaces copyWith({ + double? cardRadius, + double? compactCardRadius, + double? dialogRadius, + double? cardElevation, + Color? cardShadowColor, + Color? compactShadowColor, + }) { + return AppSurfaces( + cardRadius: cardRadius ?? this.cardRadius, + compactCardRadius: compactCardRadius ?? this.compactCardRadius, + dialogRadius: dialogRadius ?? this.dialogRadius, + cardElevation: cardElevation ?? this.cardElevation, + cardShadowColor: cardShadowColor ?? this.cardShadowColor, + compactShadowColor: compactShadowColor ?? this.compactShadowColor, + ); + } + + @override + AppSurfaces lerp(ThemeExtension? other, double t) { + if (other is! AppSurfaces) return this; + return AppSurfaces( + cardRadius: lerpDouble(cardRadius, other.cardRadius, t) ?? cardRadius, + compactCardRadius: + lerpDouble(compactCardRadius, other.compactCardRadius, t) ?? + compactCardRadius, + dialogRadius: + lerpDouble(dialogRadius, other.dialogRadius, t) ?? dialogRadius, + cardElevation: + lerpDouble(cardElevation, other.cardElevation, t) ?? cardElevation, + cardShadowColor: + Color.lerp(cardShadowColor, other.cardShadowColor, t) ?? + cardShadowColor, + compactShadowColor: + Color.lerp(compactShadowColor, other.compactShadowColor, t) ?? + compactShadowColor, + ); + } +} + +// Helper because dart:ui lerpDouble isn't exported here +double? lerpDouble(num? a, num? b, double t) { + if (a == null && b == null) return null; + a = a ?? 0; + b = b ?? 0; + return a + (b - a) * t; +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index d2ff2525..f8f91d90 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'app_typography.dart'; +import 'app_surfaces.dart'; class AppTheme { static ThemeData light() { @@ -24,10 +25,19 @@ class AppTheme { const TextStyle(letterSpacing: 0.2), ); + final surfaces = AppSurfaces( + cardRadius: 16, + compactCardRadius: 12, + dialogRadius: 20, + cardElevation: 3, + cardShadowColor: const Color.fromRGBO(0, 0, 0, 0.12), + compactShadowColor: const Color.fromRGBO(0, 0, 0, 0.08), + ); + return base.copyWith( textTheme: textTheme, scaffoldBackgroundColor: base.colorScheme.surfaceContainerLowest, - extensions: [mono], + extensions: [mono, surfaces], appBarTheme: AppBarTheme( backgroundColor: base.colorScheme.surface, foregroundColor: base.colorScheme.onSurface, @@ -142,10 +152,19 @@ class AppTheme { const TextStyle(letterSpacing: 0.2), ); + final surfaces = AppSurfaces( + cardRadius: 16, + compactCardRadius: 12, + dialogRadius: 20, + cardElevation: 3, + cardShadowColor: const Color.fromRGBO(0, 0, 0, 0.24), + compactShadowColor: const Color.fromRGBO(0, 0, 0, 0.12), + ); + return base.copyWith( textTheme: textTheme, scaffoldBackgroundColor: base.colorScheme.surface, - extensions: [mono], + extensions: [mono, surfaces], appBarTheme: AppBarTheme( backgroundColor: base.colorScheme.surface, foregroundColor: base.colorScheme.onSurface, diff --git a/lib/widgets/task_assignment_section.dart b/lib/widgets/task_assignment_section.dart index cfe4c604..d16c9825 100644 --- a/lib/widgets/task_assignment_section.dart +++ b/lib/widgets/task_assignment_section.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/profile.dart'; import '../providers/profile_provider.dart'; import '../providers/tasks_provider.dart'; +import '../theme/app_surfaces.dart'; class TaskAssignmentSection extends ConsumerWidget { const TaskAssignmentSection({ @@ -141,6 +142,7 @@ class TaskAssignmentSection extends ConsumerWidget { context: context, builder: (dialogContext) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Assign IT Staff'), content: const Text('No vacant IT staff available.'), actions: [ @@ -162,6 +164,7 @@ class TaskAssignmentSection extends ConsumerWidget { return StatefulBuilder( builder: (context, setState) { return AlertDialog( + shape: AppSurfaces.of(context).dialogShape, title: const Text('Assign IT Staff'), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 12), content: SizedBox( diff --git a/lib/widgets/tasq_adaptive_list.dart b/lib/widgets/tasq_adaptive_list.dart index e0f14554..cc7f5d3a 100644 --- a/lib/widgets/tasq_adaptive_list.dart +++ b/lib/widgets/tasq_adaptive_list.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import '../theme/app_typography.dart'; +import '../theme/app_surfaces.dart'; import 'mono_text.dart'; /// A column configuration for the [TasQAdaptiveList] desktop table view. @@ -306,13 +307,21 @@ class _MobileTile extends StatelessWidget { Widget build(BuildContext context) { final tile = mobileTileBuilder(context, item, actions); - // Apply Material 2 style elevation for Cards (per Hybrid M3/M2 guidelines) + // Apply Material 2 style elevation for Cards (per Hybrid M3/M2 guidelines). + // Mobile tiles deliberately use a slightly smaller corner radius for + // compactness, but they should inherit the global card elevation and + // shadow color from the theme to maintain visual consistency. if (tile is Card) { + final themeCard = Theme.of(context).cardTheme; return Card( color: tile.color, - elevation: 2, + elevation: themeCard.elevation ?? 3, margin: tile.margin, - shape: tile.shape, + // prefer the tile's explicit shape. For mobile tiles we intentionally + // use the compact radius token so list items feel denser while + // remaining theme-driven. + shape: tile.shape ?? AppSurfaces.of(context).compactShape, + shadowColor: AppSurfaces.of(context).compactShadowColor, clipBehavior: tile.clipBehavior, child: tile.child, ); diff --git a/test/theme_overhaul_test.dart b/test/theme_overhaul_test.dart new file mode 100644 index 00000000..0ead5c56 --- /dev/null +++ b/test/theme_overhaul_test.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tasq/theme/app_theme.dart'; +import 'package:tasq/theme/app_surfaces.dart'; +import 'package:tasq/widgets/tasq_adaptive_list.dart'; + +void main() { + testWidgets('AppTheme sets cardTheme elevation to 3 (M2-style)', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(), + home: Builder( + builder: (context) { + final elevation = Theme.of(context).cardTheme.elevation; + expect(elevation, isNotNull); + expect(elevation, inInclusiveRange(2.0, 4.0)); + expect(elevation, equals(3)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Card without explicit elevation uses theme elevation', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(), + home: const Scaffold( + body: Card(child: SizedBox(width: 20, height: 20)), + ), + ), + ); + + // Find the Material that actually paints the Card and assert elevation + final materialFinder = find.descendant( + of: find.byType(Card), + matching: find.byType(Material), + ); + + expect(materialFinder, findsWidgets); + + final material = tester.widget(materialFinder.first); + expect(material.elevation, isNotNull); + expect(material.elevation, inInclusiveRange(2.0, 4.0)); + }); + + testWidgets( + 'TasQAdaptiveList mobile Card inherits theme elevation and uses compact radius', + (WidgetTester tester) async { + final theme = AppTheme.light(); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: MediaQuery( + data: const MediaQueryData(size: Size(320, 800)), + child: Scaffold( + body: Center( + child: SizedBox( + width: 320, + child: TasQAdaptiveList( + items: const [1], + columns: const [], + mobileTileBuilder: (context, item, actions) => + const Card(child: SizedBox(width: 100, height: 40)), + ), + ), + ), + ), + ), + ), + ); + + // the Card returned by the mobile tile builder should be re-wrapped by + // _MobileTile; assert that the visible Card uses theme elevation and a + // compact 12px radius (per mobile rules). + final cardFinder = find.byType(Card); + expect(cardFinder, findsWidgets); + + final Card cardWidget = tester.widget(cardFinder.first); + final shape = cardWidget.shape as RoundedRectangleBorder; + final radius = (shape.borderRadius as BorderRadius).topLeft.x; + + expect( + radius, + equals( + AppSurfaces.of(tester.element(cardFinder.first)).compactCardRadius, + ), + ); + + // Verify the painted Material has the theme elevation + final materialFinder = find.descendant( + of: find.byType(Card), + matching: find.byType(Material), + ); + final material = tester.widget(materialFinder.first); + expect(material.elevation, equals(theme.cardTheme.elevation)); + }, + ); + + testWidgets('AppSurfaces tokens are present and dialog/card radii differ', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(), + home: Builder( + builder: (context) { + final surfaces = AppSurfaces.of(context); + expect(surfaces.compactCardRadius, lessThan(surfaces.cardRadius)); + expect(surfaces.dialogRadius, greaterThanOrEqualTo(18)); + final dialogShape = + (surfaces.dialogShape.borderRadius as BorderRadius).topLeft.x; + final compactShape = + (surfaces.compactShape.borderRadius as BorderRadius).topLeft.x; + expect(dialogShape, greaterThan(compactShape)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); +}