Created App Surfaces for Theming

This commit is contained in:
Marc Rejohn Castillano 2026-02-17 07:27:42 +08:00
parent e7c6f0d3b0
commit 7fb465f6c9
17 changed files with 336 additions and 22 deletions

View File

@ -7,6 +7,7 @@ import '../../providers/profile_provider.dart';
import '../../providers/tickets_provider.dart'; import '../../providers/tickets_provider.dart';
import '../../widgets/mono_text.dart'; import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/tasq_adaptive_list.dart';
class OfficesScreen extends ConsumerStatefulWidget { class OfficesScreen extends ConsumerStatefulWidget {
@ -177,6 +178,7 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: Text(office == null ? 'Create Office' : 'Edit Office'), title: Text(office == null ? 'Create Office' : 'Edit Office'),
content: TextField( content: TextField(
controller: nameController, controller: nameController,

View File

@ -8,6 +8,7 @@ import '../../models/user_office.dart';
import '../../providers/admin_user_provider.dart'; import '../../providers/admin_user_provider.dart';
import '../../providers/profile_provider.dart'; import '../../providers/profile_provider.dart';
import '../../providers/tickets_provider.dart'; import '../../providers/tickets_provider.dart';
import '../../theme/app_surfaces.dart';
import '../../providers/user_offices_provider.dart'; import '../../providers/user_offices_provider.dart';
import '../../utils/app_time.dart'; import '../../utils/app_time.dart';
import '../../widgets/mono_text.dart'; import '../../widgets/mono_text.dart';
@ -241,8 +242,10 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
}, },
onRequestRefresh: () { onRequestRefresh: () {
// For server-side pagination, update the query provider // For server-side pagination, update the query provider
ref.read(adminUserQueryProvider.notifier).state = ref.read(adminUserQueryProvider.notifier).state = const AdminUserQuery(
const AdminUserQuery(offset: 0, limit: 50); offset: 0,
limit: 50,
);
}, },
isLoading: false, isLoading: false,
); );
@ -289,6 +292,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setDialogState) { builder: (context, setDialogState) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Update user'), title: const Text('Update user'),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520), constraints: const BoxConstraints(maxWidth: 520),

View File

@ -10,6 +10,7 @@ import '../../providers/profile_provider.dart';
import '../../providers/tasks_provider.dart'; import '../../providers/tasks_provider.dart';
import '../../providers/tickets_provider.dart'; import '../../providers/tickets_provider.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/mono_text.dart'; import '../../widgets/mono_text.dart';
import '../../widgets/status_pill.dart'; import '../../widgets/status_pill.dart';
import '../../utils/app_time.dart'; import '../../utils/app_time.dart';
@ -497,7 +498,7 @@ class _StaffTable extends StatelessWidget {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, 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), border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
), ),
child: Column( child: Column(

View File

@ -8,6 +8,7 @@ import '../../providers/tasks_provider.dart';
import '../../providers/tickets_provider.dart'; import '../../providers/tickets_provider.dart';
import '../../widgets/mono_text.dart'; import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
class NotificationsScreen extends ConsumerWidget { class NotificationsScreen extends ConsumerWidget {
const NotificationsScreen({super.key}); const NotificationsScreen({super.key});
@ -74,7 +75,11 @@ class NotificationsScreen extends ConsumerWidget {
final title = _notificationTitle(item.type, actorName); final title = _notificationTitle(item.type, actorName);
final icon = _notificationIcon(item.type); 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( return Card(
shape: AppSurfaces.of(context).compactShape,
shadowColor: AppSurfaces.of(context).compactShadowColor,
child: ListTile( child: ListTile(
leading: Icon(icon), leading: Icon(icon),
title: Text(title), title: Text(title),

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/app_surfaces.dart';
/// Searchable multi-select dropdown with chips and 'Select All' option /// Searchable multi-select dropdown with chips and 'Select All' option
class SearchableMultiSelectDropdown<T> extends StatefulWidget { class SearchableMultiSelectDropdown<T> extends StatefulWidget {
const SearchableMultiSelectDropdown({ const SearchableMultiSelectDropdown({
@ -53,6 +55,7 @@ class SearchableMultiSelectDropdownState<T>
) )
.toList(); .toList();
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: Text('Select ${widget.label}'), title: Text('Select ${widget.label}'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
class UnderDevelopmentScreen extends StatelessWidget { class UnderDevelopmentScreen extends StatelessWidget {
@ -33,7 +34,9 @@ class UnderDevelopmentScreen extends StatelessWidget {
color: Theme.of( color: Theme.of(
context, context,
).colorScheme.primaryContainer.withValues(alpha: 0.7), ).colorScheme.primaryContainer.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(
AppSurfaces.of(context).dialogRadius,
),
), ),
child: Icon( child: Icon(
icon, icon,

View File

@ -16,6 +16,7 @@ import '../../widgets/app_breakpoints.dart';
import '../../widgets/mono_text.dart'; import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../widgets/status_pill.dart'; import '../../widgets/status_pill.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/task_assignment_section.dart'; import '../../widgets/task_assignment_section.dart';
import '../../widgets/typing_dots.dart'; import '../../widgets/typing_dots.dart';
@ -377,10 +378,18 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: bubbleColor, color: bubbleColor,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16), topLeft: Radius.circular(
topRight: const Radius.circular(16), AppSurfaces.of(context).cardRadius,
bottomLeft: Radius.circular(isMe ? 16 : 4), ),
bottomRight: Radius.circular(isMe ? 4 : 16), 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), child: _buildMentionText(message.content, textColor, profiles),
@ -695,7 +704,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
AppSurfaces.of(context).compactCardRadius,
),
), ),
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,

View File

@ -17,6 +17,7 @@ import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/tasq_adaptive_list.dart';
import '../../widgets/typing_dots.dart'; import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
class TasksListScreen extends ConsumerStatefulWidget { class TasksListScreen extends ConsumerStatefulWidget {
const TasksListScreen({super.key}); const TasksListScreen({super.key});
@ -385,6 +386,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
builder: (context, setState) { builder: (context, setState) {
final officesAsync = ref.watch(officesProvider); final officesAsync = ref.watch(officesProvider);
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Create Task'), title: const Text('Create Task'),
content: SizedBox( content: SizedBox(
width: 360, width: 360,
@ -499,7 +501,9 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
AppSurfaces.of(context).compactCardRadius,
),
), ),
child: Text( child: Text(
'#$queueOrder', '#$queueOrder',
@ -691,6 +695,8 @@ class _StatusSummaryCard extends StatelessWidget {
return Card( return Card(
color: background, color: background,
// summary cards are compact use compact token for consistent density
shape: AppSurfaces.of(context).compactShape,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column( child: Column(

View File

@ -7,6 +7,7 @@ import '../../providers/teams_provider.dart';
import '../../providers/profile_provider.dart'; import '../../providers/profile_provider.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tasq/screens/searchable_multi_select_dropdown.dart'; import 'package:tasq/screens/searchable_multi_select_dropdown.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/tasq_adaptive_list.dart';
final officesProvider = FutureProvider<List<Office>>((ref) async { final officesProvider = FutureProvider<List<Office>>((ref) async {
@ -150,6 +151,7 @@ class TeamsScreen extends ConsumerWidget {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: Text(isEdit ? 'Edit Team' : 'Add Team'), title: Text(isEdit ? 'Edit Team' : 'Add Team'),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
@ -266,6 +268,7 @@ class TeamsScreen extends ConsumerWidget {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Delete Team'), title: const Text('Delete Team'),
content: const Text('Are you sure you want to delete this team?'), content: const Text('Are you sure you want to delete this team?'),
actions: [ actions: [

View File

@ -19,6 +19,7 @@ import '../../widgets/responsive_body.dart';
import '../../widgets/status_pill.dart'; import '../../widgets/status_pill.dart';
import '../../widgets/task_assignment_section.dart'; import '../../widgets/task_assignment_section.dart';
import '../../widgets/typing_dots.dart'; import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
class TicketDetailScreen extends ConsumerStatefulWidget { class TicketDetailScreen extends ConsumerStatefulWidget {
const TicketDetailScreen({super.key, required this.ticketId}); const TicketDetailScreen({super.key, required this.ticketId});
@ -233,13 +234,25 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: bubbleColor, color: bubbleColor,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16), topLeft: Radius.circular(
topRight: const Radius.circular(16), AppSurfaces.of(context).cardRadius,
),
topRight: Radius.circular(
AppSurfaces.of(context).cardRadius,
),
bottomLeft: Radius.circular( bottomLeft: Radius.circular(
isMe ? 16 : 4, isMe
? AppSurfaces.of(
context,
).cardRadius
: 4,
), ),
bottomRight: Radius.circular( bottomRight: Radius.circular(
isMe ? 4 : 16, isMe
? 4
: AppSurfaces.of(
context,
).cardRadius,
), ),
), ),
), ),
@ -609,7 +622,9 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
AppSurfaces.of(context).compactCardRadius,
),
), ),
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
@ -811,6 +826,7 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Ticket Timeline'), title: const Text('Ticket Timeline'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -929,7 +945,9 @@ class _MetaBadge extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: background, color: background,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
AppSurfaces.of(context).compactCardRadius,
),
border: Border.all(color: border), border: Border.all(color: border),
), ),
child: Row( child: Row(

View File

@ -15,6 +15,7 @@ import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/tasq_adaptive_list.dart';
import '../../widgets/typing_dots.dart'; import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
class TicketsListScreen extends ConsumerStatefulWidget { class TicketsListScreen extends ConsumerStatefulWidget {
const TicketsListScreen({super.key}); const TicketsListScreen({super.key});
@ -321,6 +322,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Create Ticket'), title: const Text('Create Ticket'),
content: Consumer( content: Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
@ -582,6 +584,8 @@ class _StatusSummaryCard extends StatelessWidget {
return Card( return Card(
color: background, color: background,
// summary cards are compact use compact token for consistent density
shape: AppSurfaces.of(context).compactShape,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column( child: Column(

View File

@ -12,6 +12,7 @@ import '../../providers/profile_provider.dart';
import '../../providers/workforce_provider.dart'; import '../../providers/workforce_provider.dart';
import '../../utils/app_time.dart'; import '../../utils/app_time.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
class WorkforceScreen extends ConsumerWidget { class WorkforceScreen extends ConsumerWidget {
const WorkforceScreen({super.key}); const WorkforceScreen({super.key});
@ -477,6 +478,7 @@ class _ScheduleTile extends ConsumerWidget {
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Request swap'), title: const Text('Request swap'),
content: DropdownButtonFormField<String>( content: DropdownButtonFormField<String>(
initialValue: selectedId, initialValue: selectedId,
@ -560,6 +562,7 @@ class _ScheduleTile extends ConsumerWidget {
completer.complete(dialogContext); completer.complete(dialogContext);
} }
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Validating location'), title: const Text('Validating location'),
content: Row( content: Row(
children: [ children: [
@ -894,7 +897,9 @@ class _ScheduleGeneratorPanelState
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.tertiaryContainer, color: colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
AppSurfaces.of(context).compactCardRadius,
),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
@immutable
class AppSurfaces extends ThemeExtension<AppSurfaces> {
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<AppSurfaces>();
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<AppSurfaces>? 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;
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'app_typography.dart'; import 'app_typography.dart';
import 'app_surfaces.dart';
class AppTheme { class AppTheme {
static ThemeData light() { static ThemeData light() {
@ -24,10 +25,19 @@ class AppTheme {
const TextStyle(letterSpacing: 0.2), 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( return base.copyWith(
textTheme: textTheme, textTheme: textTheme,
scaffoldBackgroundColor: base.colorScheme.surfaceContainerLowest, scaffoldBackgroundColor: base.colorScheme.surfaceContainerLowest,
extensions: [mono], extensions: [mono, surfaces],
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: base.colorScheme.surface, backgroundColor: base.colorScheme.surface,
foregroundColor: base.colorScheme.onSurface, foregroundColor: base.colorScheme.onSurface,
@ -142,10 +152,19 @@ class AppTheme {
const TextStyle(letterSpacing: 0.2), 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( return base.copyWith(
textTheme: textTheme, textTheme: textTheme,
scaffoldBackgroundColor: base.colorScheme.surface, scaffoldBackgroundColor: base.colorScheme.surface,
extensions: [mono], extensions: [mono, surfaces],
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: base.colorScheme.surface, backgroundColor: base.colorScheme.surface,
foregroundColor: base.colorScheme.onSurface, foregroundColor: base.colorScheme.onSurface,

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/profile.dart'; import '../models/profile.dart';
import '../providers/profile_provider.dart'; import '../providers/profile_provider.dart';
import '../providers/tasks_provider.dart'; import '../providers/tasks_provider.dart';
import '../theme/app_surfaces.dart';
class TaskAssignmentSection extends ConsumerWidget { class TaskAssignmentSection extends ConsumerWidget {
const TaskAssignmentSection({ const TaskAssignmentSection({
@ -141,6 +142,7 @@ class TaskAssignmentSection extends ConsumerWidget {
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Assign IT Staff'), title: const Text('Assign IT Staff'),
content: const Text('No vacant IT staff available.'), content: const Text('No vacant IT staff available.'),
actions: [ actions: [
@ -162,6 +164,7 @@ class TaskAssignmentSection extends ConsumerWidget {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Assign IT Staff'), title: const Text('Assign IT Staff'),
contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 12), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 12),
content: SizedBox( content: SizedBox(

View File

@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/app_typography.dart'; import '../theme/app_typography.dart';
import '../theme/app_surfaces.dart';
import 'mono_text.dart'; import 'mono_text.dart';
/// A column configuration for the [TasQAdaptiveList] desktop table view. /// A column configuration for the [TasQAdaptiveList] desktop table view.
@ -306,13 +307,21 @@ class _MobileTile<T> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tile = mobileTileBuilder(context, item, actions); 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) { if (tile is Card) {
final themeCard = Theme.of(context).cardTheme;
return Card( return Card(
color: tile.color, color: tile.color,
elevation: 2, elevation: themeCard.elevation ?? 3,
margin: tile.margin, 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, clipBehavior: tile.clipBehavior,
child: tile.child, child: tile.child,
); );

View File

@ -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<Material>(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<int>(
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<Card>(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<Material>(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();
},
),
),
);
});
}