Created App Surfaces for Theming
This commit is contained in:
parent
e7c6f0d3b0
commit
7fb465f6c9
|
|
@ -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<OfficesScreen> {
|
|||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: Text(office == null ? 'Create Office' : 'Edit Office'),
|
||||
content: TextField(
|
||||
controller: nameController,
|
||||
|
|
|
|||
|
|
@ -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<UserManagementScreen> {
|
|||
},
|
||||
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<UserManagementScreen> {
|
|||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: const Text('Update user'),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<T> extends StatefulWidget {
|
||||
const SearchableMultiSelectDropdown({
|
||||
|
|
@ -53,6 +55,7 @@ class SearchableMultiSelectDropdownState<T>
|
|||
)
|
||||
.toList();
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: Text('Select ${widget.label}'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<TaskDetailScreen> {
|
|||
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<TaskDetailScreen> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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<TasksListScreen> {
|
|||
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<TasksListScreen> {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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<List<Office>>((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<bool>(
|
||||
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: [
|
||||
|
|
|
|||
|
|
@ -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<TicketDetailScreen> {
|
|||
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<TicketDetailScreen> {
|
|||
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<TicketDetailScreen> {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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<TicketsListScreen> {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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<String>(
|
||||
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,
|
||||
|
|
|
|||
90
lib/theme/app_surfaces.dart
Normal file
90
lib/theme/app_surfaces.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<T> 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,
|
||||
);
|
||||
|
|
|
|||
128
test/theme_overhaul_test.dart
Normal file
128
test/theme_overhaul_test.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user