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 '../../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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
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 '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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
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