diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index f3d683a..5572bf3 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -508,6 +508,12 @@ Files backup/restore - mirror mode or zip --> +# DOCUMENTS SCREEN +- redesign the tab and its top header, make it simple and responsive, make the design similar to the multi select menu (done) -> improve the design for lower end android, add redesign the list view and its grid (done) + +# ARCHIVES SCREEN +- redesign the tab and its top header, make it simple and responsive, make the design similar to the multi select menu (done) -> improve the design for lower end android, add redesign the list view and its grid (done) + --- # DONT DO diff --git a/frontend/lib/ui/screens/archives/archives_screen.dart b/frontend/lib/ui/screens/archives/archives_screen.dart index 809e7b2..4653e79 100644 --- a/frontend/lib/ui/screens/archives/archives_screen.dart +++ b/frontend/lib/ui/screens/archives/archives_screen.dart @@ -838,52 +838,11 @@ class _ArchivesScreenState extends ConsumerState else _buildTopHeader(isMobile, isFolderOpened, query), - // ── TabBar ── - Container( - color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - child: TabBar( - controller: _tabController, - onTap: (index) { - if (index == _tabController.index && - index == 0 && - _openedFolderStudentId != null) { - setState(() { - _openedFolderStudentId = null; - _openedFolderName = null; - }); - ref - .read(archiveDocumentQueryProvider.notifier) - .setStudentId(null); - _clearFilters(); - ref.invalidate(archiveDocumentPageProvider); - ref.invalidate(archiveStudentFoldersProvider); - } - }, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: Theme.of(context).brightness == Brightness.dark ? AppColors.darkTextSecondary : AppColors.textSecondary, - indicatorColor: AppColors.primaryGreen, - indicatorWeight: 2.5, - labelStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13, - ), - tabs: const [ - Tab( - text: 'Student Folders', - ), - Tab( - text: 'All Archived Docs', - ), - ], - ), - ), - - Divider(height: 1, color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkBorder : Colors.grey.shade200), - // ── Tab Body ── Expanded( child: TabBarView( controller: _tabController, + physics: const NeverScrollableScrollPhysics(), children: [ // Tab 0: Student Folders _buildFoldersTab(foldersAsync, docState, query, isMobile), @@ -946,131 +905,397 @@ class _ArchivesScreenState extends ConsumerState } // ════════════════════════════════════════════════════════════════ - // TOP HEADER + // TOP HEADER (Unified with Segmented Tabs, styled like Multi-Select) // ════════════════════════════════════════════════════════════════ + void _closeFolder() { + setState(() { + _openedFolderStudentId = null; + _openedFolderName = null; + _isMultiSelectMode = false; + _selectedDocumentIds.clear(); + }); + ref.read(openedArchiveFolderProvider.notifier).setFolder(null); + ref.read(archiveDocumentQueryProvider.notifier).setStudentId(null); + _clearFilters(); + ref.invalidate(archiveDocumentPageProvider); + ref.invalidate(archiveStudentFoldersProvider); + } + + Widget _buildSegmentedTabSwitcher( + bool isDark, + bool isMobile, + bool isMobileOrAndroid, + ) { + final activeIndex = _tabController.index; + return Container( + height: 36, + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurface2 : const Color(0xFFF1F3F5), + borderRadius: BorderRadius.circular(9), + border: Border.all( + color: isDark ? AppColors.darkBorder : const Color(0xFFE9ECEF), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildSegmentedTabItem( + index: 0, + icon: Icons.folder_outlined, + activeIcon: Icons.folder_rounded, + label: isMobile ? 'Folders' : 'Student Folders', + isSelected: activeIndex == 0, + isDark: isDark, + isMobile: isMobile, + isMobileOrAndroid: isMobileOrAndroid, + ), + const SizedBox(width: 2), + _buildSegmentedTabItem( + index: 1, + icon: Icons.inventory_2_outlined, + activeIcon: Icons.inventory_2_rounded, + label: isMobile ? 'Archived' : 'All Archived Docs', + isSelected: activeIndex == 1, + isDark: isDark, + isMobile: isMobile, + isMobileOrAndroid: isMobileOrAndroid, + ), + ], + ), + ); + } + + Widget _buildSegmentedTabItem({ + required int index, + required IconData icon, + required IconData activeIcon, + required String label, + required bool isSelected, + required bool isDark, + required bool isMobile, + required bool isMobileOrAndroid, + }) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(7), + onTap: () { + if (_tabController.index == index) { + if (index == 0 && _openedFolderStudentId != null) { + _closeFolder(); + } + } else { + _tabController.animateTo(index); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeInOut, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 8 : 13, + vertical: 4, + ), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryGreen : Colors.transparent, + borderRadius: BorderRadius.circular(7), + boxShadow: isSelected && !isDark && !isMobileOrAndroid + ? [ + BoxShadow( + color: AppColors.primaryGreen.withValues(alpha: 0.25), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ] + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? activeIcon : icon, + size: 15, + color: isSelected + ? Colors.white + : (isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary), + ), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + fontSize: isMobile ? 12 : 12.5, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected + ? Colors.white + : (isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary), + ), + ), + ], + ), + ), + ), + ); + } + Widget _buildTopHeader( bool isMobile, bool isFolderOpened, ArchiveDocumentQueryParams query, ) { - final hPad = isMobile ? 12.0 : 20.0; final isDark = Theme.of(context).brightness == Brightness.dark; - return Container( - color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - padding: EdgeInsets.symmetric(horizontal: hPad, vertical: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Back button if folder opened - if (isFolderOpened) ...[ - IconButton( - icon: const Icon( - Icons.arrow_back_ios_new_rounded, - size: 20, - ), - color: AppColors.primaryGreen, - tooltip: 'Back to Folders', - onPressed: () { - setState(() { - _openedFolderStudentId = null; - _openedFolderName = null; - _isMultiSelectMode = false; - _selectedDocumentIds.clear(); - }); - ref - .read(archiveDocumentQueryProvider.notifier) - .setStudentId(null); - }, - ), - const SizedBox(width: 6), - ], + final isMobileOrAndroid = + isMobile || defaultTargetPlatform == TargetPlatform.android; - // Screen title - Expanded( - child: Text( - _openedFolderName ?? 'System Archive', - style: TextStyle( - fontSize: isMobile ? 17 : 21, - fontWeight: FontWeight.bold, - color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, - ), - overflow: TextOverflow.ellipsis, - ), + return RepaintBoundary( + child: Container( + height: 52, + margin: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 20, + vertical: 8, + ), + padding: EdgeInsets.symmetric(horizontal: isMobile ? 8 : 12), + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurfaceCard : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? AppColors.darkBorder : AppColors.borderLight, + width: 1.0, ), - - if (!isFolderOpened) ...[ - Tooltip( - richMessage: const TextSpan( - text: 'Search Archives ', - children: [ - TextSpan( - text: '(Ctrl+F)', - style: TextStyle(fontStyle: FontStyle.italic), + boxShadow: isMobileOrAndroid + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), ), ], - ), - child: IconButton( - icon: Icon( - Icons.search, - size: 28, - color: isDark ? AppColors.darkTextPrimary : Colors.black87, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left side: Folder breadcrumb or Segmented Tabs + if (isFolderOpened) ...[ + IconButton( + icon: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 18, ), - onPressed: () => _showSearchDialog(context), + color: AppColors.primaryGreen, + tooltip: 'Back to Folders', + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minWidth: 34, minHeight: 34), + onPressed: _closeFolder, ), - ), - const SizedBox(width: 8), - ], + const SizedBox(width: 4), + Expanded( + child: Row( + children: [ + if (!isMobile) ...[ + InkWell( + onTap: _closeFolder, + child: Text( + 'Student Folders', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary, + ), + ), + ), + Text( + ' / ', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary, + ), + ), + ], + Flexible( + child: Text( + _openedFolderName!, + style: TextStyle( + fontSize: isMobile ? 14.5 : 15, + fontWeight: FontWeight.bold, + color: isDark + ? AppColors.darkTextPrimary + : AppColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ] else ...[ + _buildSegmentedTabSwitcher(isDark, isMobile, isMobileOrAndroid), + const Spacer(), + ], - if (_tabController.index == 1 || isFolderOpened) ...[ - Tooltip( - message: 'Filter Documents', - child: IconButton( - onPressed: _openFilterDialog, - icon: Badge( - isLabelVisible: _getActiveFilterCount() > 0, - label: Text(_getActiveFilterCount().toString()), - child: Icon( - Icons.tune_rounded, + // Action buttons + if (!isFolderOpened) ...[ + Tooltip( + richMessage: (_searchController.text.isNotEmpty || + query.search.isNotEmpty) + ? const TextSpan(text: 'Clear Search') + : const TextSpan( + text: 'Search Archives ', + children: [ + TextSpan( + text: '(Ctrl+F)', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + child: IconButton( + icon: Icon( + (_searchController.text.isNotEmpty || + query.search.isNotEmpty) + ? Icons.close + : Icons.search, size: 20, - color: _getActiveFilterCount() > 0 + color: isDark ? AppColors.darkTextPrimary : Colors.black87, + ), + padding: const EdgeInsets.all(8), + constraints: + const BoxConstraints(minWidth: 36, minHeight: 36), + onPressed: () { + if (_searchController.text.isNotEmpty || + query.search.isNotEmpty) { + _searchController.clear(); + ref + .read(archiveDocumentQueryProvider.notifier) + .setSearch(''); + setState(() => _foldersPage = 1); + ref.invalidate(archiveStudentFoldersProvider); + ref.invalidate(archiveDocumentPageProvider); + } else { + _showSearchDialog(context); + } + }, + ), + ), + ], + + const SizedBox(width: 2), + + // Multi-Select Toggle (Desktop non-Windows only, icon only) + if (!isMobile && + defaultTargetPlatform != TargetPlatform.windows && + widget.userRole != 'teacher' && + (_tabController.index == 1 || isFolderOpened)) ...[ + Tooltip( + message: _isMultiSelectMode + ? 'Exit Multi-Select' + : 'Multi-Select', + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.transparent, + side: BorderSide.none, + shadowColor: Colors.transparent, + padding: const EdgeInsets.all(8), + ), + constraints: + const BoxConstraints(minWidth: 36, minHeight: 36), + onPressed: () { + setState(() { + _isMultiSelectMode = !_isMultiSelectMode; + if (!_isMultiSelectMode) _selectedDocumentIds.clear(); + }); + }, + icon: Icon( + Icons.checklist_rounded, + size: 20, + color: _isMultiSelectMode ? AppColors.primaryGreen - : (isDark ? AppColors.darkTextPrimary : Colors.black87), + : (isDark + ? AppColors.darkTextPrimary + : AppColors.textSecondary), ), ), ), - ), - const SizedBox(width: 4), - ], + const SizedBox(width: 2), + ], - if (defaultTargetPlatform != TargetPlatform.android && !isMobile && widget.userRole != 'teacher') ...[ + // Filter button (Icon only, between Search and Print) + if (_tabController.index == 1 || isFolderOpened) ...[ + Tooltip( + message: 'Filter Documents', + child: IconButton( + padding: const EdgeInsets.all(8), + constraints: + const BoxConstraints(minWidth: 36, minHeight: 36), + onPressed: _openFilterDialog, + icon: Badge( + isLabelVisible: _getActiveFilterCount() > 0, + label: Text(_getActiveFilterCount().toString()), + child: Icon( + Icons.tune_rounded, + size: 20, + color: _getActiveFilterCount() > 0 + ? AppColors.primaryGreen + : (isDark + ? AppColors.darkTextPrimary + : Colors.black87), + ), + ), + ), + ), + const SizedBox(width: 2), + ], + + // Print List Button (Desktop non-mobile) + if (defaultTargetPlatform != TargetPlatform.android && + !isMobile && + widget.userRole != 'teacher') ...[ + SizedBox( + height: 36, + child: _buildPrintQueueButton(compact: false), + ), + const SizedBox(width: 6), + ], + + // Dropdown Menu SizedBox( height: 36, - child: _buildPrintQueueButton(compact: false), + width: 36, + child: _buildMoreOptionsDropdown(isMobile), ), - const SizedBox(width: 8), + + if (!isMobile) ...[ + const SizedBox(width: 2), + // Info Button for Download Guide (Desktop) + IconButton( + icon: const Icon( + Icons.info_outline, + color: AppColors.primaryGreen, + size: 20, + ), + padding: const EdgeInsets.all(8), + constraints: + const BoxConstraints(minWidth: 36, minHeight: 36), + tooltip: 'Download Guide', + onPressed: () { + showDialog( + context: context, + builder: (_) => const DownloadGuideDialog(), + ); + }, + ), + ], ], - - // Dropdown Menu - SizedBox(height: 38, child: _buildMoreOptionsDropdown(isMobile)), - - const SizedBox(width: 4), - - // Info Button for Download Guide (Moved to right end) - IconButton( - icon: const Icon( - Icons.info_outline, - color: AppColors.primaryGreen, - size: 20, - ), - tooltip: 'Download Guide', - onPressed: () { - showDialog( - context: context, - builder: (_) => const DownloadGuideDialog(), - ); - }, - ), - ], + ), ), ); } @@ -1659,6 +1884,11 @@ class _ArchivesScreenState extends ConsumerState context: context, builder: (_) => const RecycleBinModal(), ); + } else if (value == 'download_guide') { + showDialog( + context: context, + builder: (_) => const DownloadGuideDialog(), + ); } }, itemBuilder: (context) => [ @@ -1726,6 +1956,23 @@ class _ArchivesScreenState extends ConsumerState ], ), ), + if (isMobile) ...[ + const PopupMenuDivider(), + const PopupMenuItem( + value: 'download_guide', + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 20, + color: AppColors.primaryGreen, + ), + SizedBox(width: 8), + Text('Download Guide', style: TextStyle(fontSize: 14)), + ], + ), + ), + ], ], ); } @@ -1811,13 +2058,20 @@ class _ArchivesScreenState extends ConsumerState decoration: BoxDecoration( color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], + border: isMobile + ? Border.all( + color: isDark ? AppColors.darkBorder : Colors.grey.shade200, + ) + : null, + boxShadow: isMobile + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), @@ -1836,86 +2090,100 @@ class _ArchivesScreenState extends ConsumerState final studentName = '${folder.studentLastName ?? ''}, ${folder.studentFirstName ?? ''}'; - return GestureDetector( - onSecondaryTapDown: (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - onLongPressStart: (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - child: InkWell( - onTap: () { - if (folder.studentId != null) { - setState(() { - _openedFolderStudentId = folder.studentId; - _openedFolderName = studentName; - }); - ref - .read(archiveDocumentQueryProvider.notifier) - .setStudentId(folder.studentId); - } - }, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 16, - vertical: isMobile ? 10 : 12, - ), - child: Row( - children: [ - const Icon( - Icons.folder_special_rounded, - size: 28, - color: Colors.deepOrange, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - studentName, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13, - color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + return RepaintBoundary( + child: GestureDetector( + onSecondaryTapDown: (details) => _showFolderContextMenu( + details.globalPosition, + folder, + ), + onLongPressStart: (details) => _showFolderContextMenu( + details.globalPosition, + folder, + ), + child: InkWell( + onTap: () { + if (folder.studentId != null) { + setState(() { + _openedFolderStudentId = folder.studentId; + _openedFolderName = studentName; + }); + ref + .read(archiveDocumentQueryProvider.notifier) + .setStudentId(folder.studentId); + } + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 16, + vertical: isMobile ? 10 : 12, + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.deepOrange.withValues( + alpha: isDark ? 0.16 : 0.10, + ), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Icon( + Icons.folder_special_rounded, + size: 22, + color: Colors.deepOrange, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + studentName, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 3), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${folder.documentCount ?? 0} ${folder.documentCount == 1 ? "item" : "items"}', - style: TextStyle( - fontSize: 11, - color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + const SizedBox(height: 3), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${folder.documentCount ?? 0} ${folder.documentCount == 1 ? "item" : "items"}', + style: TextStyle( + fontSize: 11, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), ), - ), - const SizedBox(width: 8), - _buildStudentStatusChip( - folder.studentStatus ?? 'Archived', - ), - ], - ), - ], + const SizedBox(width: 8), + _buildStudentStatusChip( + folder.studentStatus ?? 'Archived', + ), + ], + ), + ], + ), ), - ), - if (_isAdmin) - _buildFolderActionMenu( - folder.studentId!, - studentName, + if (_isAdmin) + _buildFolderActionMenu( + folder.studentId!, + studentName, + ), + Icon( + Icons.chevron_right, + size: 18, + color: isDark ? AppColors.darkTextMuted : AppColors.textMuted, ), - Icon( - Icons.chevron_right, - size: 18, - color: isDark ? AppColors.darkTextMuted : AppColors.textMuted, - ), - ], + ], + ), ), ), ), @@ -1950,83 +2218,87 @@ class _ArchivesScreenState extends ConsumerState final folder = paginatedFolders[i]; final studentName = '${folder.studentLastName ?? ''}, ${folder.studentFirstName ?? ''}'; - return GestureDetector( - onSecondaryTapDown: (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - onLongPressStart: (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - child: InkWell( - onTap: () { - if (folder.studentId != null) { - setState(() { - _openedFolderStudentId = folder.studentId; - _openedFolderName = studentName; - }); - ref - .read(archiveDocumentQueryProvider.notifier) - .setStudentId(folder.studentId); - } - }, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: isDark ? AppColors.darkBorder : Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.03), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.folder_special_rounded, - size: isMobile ? 38 : 46, - color: Colors.deepOrange, - ), - SizedBox(height: isMobile ? 6 : 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - studentName, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 12 : 13, - height: 1.2, - color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + return RepaintBoundary( + child: GestureDetector( + onSecondaryTapDown: (details) => _showFolderContextMenu( + details.globalPosition, + folder, + ), + onLongPressStart: (details) => _showFolderContextMenu( + details.globalPosition, + folder, + ), + child: InkWell( + onTap: () { + if (folder.studentId != null) { + setState(() { + _openedFolderStudentId = folder.studentId; + _openedFolderName = studentName; + }); + ref + .read(archiveDocumentQueryProvider.notifier) + .setStudentId(folder.studentId); + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isDark ? AppColors.darkBorder : Colors.grey.shade200), + boxShadow: isMobile + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.folder_special_rounded, + size: isMobile ? 38 : 46, + color: Colors.deepOrange, + ), + SizedBox(height: isMobile ? 6 : 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + studentName, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isMobile ? 12 : 13, + height: 1.2, + color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + ), ), ), - ), - const SizedBox(height: 4), - _buildStudentStatusChip( - folder.studentStatus ?? 'Archived', - ), - const SizedBox(height: 2), - Text( - '${folder.documentCount ?? 0} docs', - style: TextStyle( - fontSize: 11, - color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + const SizedBox(height: 4), + _buildStudentStatusChip( + folder.studentStatus ?? 'Archived', ), - ), - ], + const SizedBox(height: 2), + Text( + '${folder.documentCount ?? 0} docs', + style: TextStyle( + fontSize: 11, + color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, + ), + ), + ], + ), ), ), ), @@ -2244,13 +2516,20 @@ class _ArchivesScreenState extends ConsumerState decoration: BoxDecoration( color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], + border: isMobile + ? Border.all( + color: isDark ? AppColors.darkBorder : Colors.grey.shade200, + ) + : null, + boxShadow: isMobile + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), diff --git a/frontend/lib/ui/screens/documents/documents_screen.dart b/frontend/lib/ui/screens/documents/documents_screen.dart index ffb51b5..55e211d 100644 --- a/frontend/lib/ui/screens/documents/documents_screen.dart +++ b/frontend/lib/ui/screens/documents/documents_screen.dart @@ -674,7 +674,6 @@ class _DocumentsScreenState extends ConsumerState @override Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; final docState = ref.watch(documentPageProvider); final query = ref.watch(documentQueryProvider); final requirementsAsync = ref.watch(documentRequirementsProvider); @@ -801,51 +800,6 @@ class _DocumentsScreenState extends ConsumerState statusesAsync, ), - // ── TabBar ── - Container( - color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - child: TabBar( - controller: _tabController, - onTap: (index) { - // Only run reset behavior if tapping the already active tab - if (index == _tabController.index) { - if (index == 0 && _openedFolderStudentId != null) { - setState(() { - _openedFolderStudentId = null; - _openedFolderName = null; - }); - ref.read(openedFolderProvider.notifier).setFolder(null); - ref - .read(documentQueryProvider.notifier) - .setStudentId(null); - _clearFilters(); - ref.invalidate(documentPageProvider); - ref.invalidate(foldersProvider); - ref.invalidate(studentFoldersProvider); - } - } - }, - labelColor: AppColors.primaryGreen, - unselectedLabelColor: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, - indicatorColor: AppColors.primaryGreen, - indicatorWeight: 2.5, - labelStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13, - ), - tabs: const [ - Tab( - text: 'Student Folders', - ), - Tab( - text: 'All Documents', - ), - ], - ), - ), - - const Divider(height: 1), - // ── Tab Body ── Expanded( child: Stack( @@ -955,8 +909,138 @@ class _DocumentsScreenState extends ConsumerState } // ══════════════════════════════════════════════════════════════ - // TOP HEADER + // TOP HEADER (Unified with Segmented Tabs, styled like Multi-Select) // ══════════════════════════════════════════════════════════════ + void _closeFolder() { + setState(() { + _openedFolderStudentId = null; + _openedFolderName = null; + }); + ref.read(openedFolderProvider.notifier).setFolder(null); + ref.read(documentQueryProvider.notifier).setStudentId(null); + _clearFilters(); + ref.invalidate(documentPageProvider); + ref.invalidate(foldersProvider); + ref.invalidate(studentFoldersProvider); + } + + Widget _buildSegmentedTabSwitcher(bool isDark, bool isMobile, bool isMobileOrAndroid) { + final activeIndex = _tabController.index; + return Container( + height: 36, + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurface2 : const Color(0xFFF1F3F5), + borderRadius: BorderRadius.circular(9), + border: Border.all( + color: isDark ? AppColors.darkBorder : const Color(0xFFE9ECEF), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildSegmentedTabItem( + index: 0, + icon: Icons.folder_outlined, + activeIcon: Icons.folder_rounded, + label: isMobile ? 'Folders' : 'Student Folders', + isSelected: activeIndex == 0, + isDark: isDark, + isMobile: isMobile, + isMobileOrAndroid: isMobileOrAndroid, + ), + const SizedBox(width: 2), + _buildSegmentedTabItem( + index: 1, + icon: Icons.description_outlined, + activeIcon: Icons.description_rounded, + label: isMobile ? 'Documents' : 'All Documents', + isSelected: activeIndex == 1, + isDark: isDark, + isMobile: isMobile, + isMobileOrAndroid: isMobileOrAndroid, + ), + ], + ), + ); + } + + Widget _buildSegmentedTabItem({ + required int index, + required IconData icon, + required IconData activeIcon, + required String label, + required bool isSelected, + required bool isDark, + required bool isMobile, + required bool isMobileOrAndroid, + }) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(7), + onTap: () { + if (_tabController.index == index) { + if (index == 0 && _openedFolderStudentId != null) { + _closeFolder(); + } + } else { + _tabController.animateTo(index); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeInOut, + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 8 : 13, + vertical: 4, + ), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryGreen : Colors.transparent, + borderRadius: BorderRadius.circular(7), + boxShadow: isSelected && !isDark && !isMobileOrAndroid + ? [ + BoxShadow( + color: AppColors.primaryGreen.withValues(alpha: 0.25), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ] + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? activeIcon : icon, + size: 15, + color: isSelected + ? Colors.white + : (isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary), + ), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + fontSize: isMobile ? 12 : 12.5, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected + ? Colors.white + : (isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary), + ), + ), + ], + ), + ), + ), + ); + } + Widget _buildTopHeader( bool isMobile, bool isStudentFiltered, @@ -966,284 +1050,294 @@ class _DocumentsScreenState extends ConsumerState AsyncValue> academicYearsAsync, AsyncValue> statusesAsync, ) { - final hPad = isMobile ? 12.0 : 20.0; - final isDark = Theme.of(context).brightness == Brightness.dark; + final isMobileOrAndroid = isMobile || defaultTargetPlatform == TargetPlatform.android; - return Container( - color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - padding: EdgeInsets.symmetric(horizontal: hPad, vertical: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Back button if folder opened - if (isFolderOpened) ...[ - IconButton( - icon: const Icon( - Icons.arrow_back_ios_new_rounded, - size: 20, + return RepaintBoundary( + child: Container( + height: 52, + margin: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 20, + vertical: 8, + ), + padding: EdgeInsets.symmetric(horizontal: isMobile ? 8 : 12), + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurfaceCard : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? AppColors.darkBorder : AppColors.borderLight, + width: 1.0, + ), + boxShadow: isMobileOrAndroid + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left side: Folder breadcrumb or Segmented Tabs + if (isFolderOpened) ...[ + IconButton( + icon: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 18, + ), + color: AppColors.primaryGreen, + tooltip: 'Back to Folders', + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minWidth: 34, minHeight: 34), + onPressed: _closeFolder, ), - color: AppColors.primaryGreen, - tooltip: 'Back to Folders', - onPressed: () { - setState(() { - _openedFolderStudentId = null; - _openedFolderName = null; - }); - ref.read(openedFolderProvider.notifier).setFolder(null); - ref.read(documentQueryProvider.notifier).setStudentId(null); - }, - ), - const SizedBox(width: 6), - ], - - // Screen title - Expanded( - child: _openedFolderName != null - ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isMobile) ...[ - InkWell( - onTap: () { - setState(() { - _openedFolderStudentId = null; - _openedFolderName = null; - }); - ref - .read(openedFolderProvider.notifier) - .setFolder(null); - ref - .read(documentQueryProvider.notifier) - .setStudentId(null); - }, - child: Text( - 'Student Folders', - style: TextStyle( - fontSize: 21, - fontWeight: FontWeight.w500, - color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, - ), - ), - ), - Text( - ' / ', - style: TextStyle( - fontSize: 21, - fontWeight: FontWeight.w500, - color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, - ), - ), - ], - Expanded( + const SizedBox(width: 4), + Expanded( + child: Row( + children: [ + if (!isMobile) ...[ + InkWell( + onTap: _closeFolder, child: Text( - _openedFolderName!, + 'Student Folders', style: TextStyle( - fontSize: isMobile ? 17 : 21, - fontWeight: FontWeight.bold, - color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary, ), ), ), - ], - ) - : Text( - _tabController.index == 0 - ? 'Student Folders' - : _tabController.index == 1 - ? (isStudentFiltered - ? 'Student Documents' - : 'All Documents') - : 'Recycle Bin', - style: TextStyle( - fontSize: isMobile ? 17 : 21, - fontWeight: FontWeight.bold, - color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, - ), - ), - ), - - const SizedBox(width: 16), - - // Clear student filter chip (compact) - if (isStudentFiltered && !isFolderOpened) ...[ - const SizedBox(width: 12), - Flexible( - child: TextButton.icon( - onPressed: () => ref - .read(documentQueryProvider.notifier) - .setStudentId(null), - icon: const Icon(Icons.close, size: 14), - label: const Text( - 'All Students', - style: TextStyle(fontSize: 12), - ), - style: TextButton.styleFrom( - foregroundColor: AppColors.primaryGreen, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ), - ), - ], - - if (!isFolderOpened) ...[ - Tooltip( - richMessage: (_searchController.text.isNotEmpty || query.search.isNotEmpty) - ? const TextSpan(text: 'Clear Search') - : const TextSpan( - text: 'Search Documents ', - children: [ - TextSpan( - text: '(Ctrl+F)', - style: TextStyle(fontStyle: FontStyle.italic), + Text( + ' / ', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary, ), - ], + ), + ], + Flexible( + child: Text( + _openedFolderName!, + style: TextStyle( + fontSize: isMobile ? 14.5 : 15, + fontWeight: FontWeight.bold, + color: isDark + ? AppColors.darkTextPrimary + : AppColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - child: IconButton( - icon: Icon( - (_searchController.text.isNotEmpty || query.search.isNotEmpty) - ? Icons.close - : Icons.search, - size: 28, - color: isDark ? AppColors.darkTextPrimary : Colors.black87, - ), - onPressed: () { - if (_searchController.text.isNotEmpty || - query.search.isNotEmpty) { - _searchController.clear(); - ref.read(documentQueryProvider.notifier).setSearch(''); - setState(() => _foldersPage = 1); - ref.invalidate(foldersProvider); - ref.invalidate(studentFoldersProvider); - ref.invalidate(documentPageProvider); - } else { - _showSearchDialog(context); - } - }, - ), - ), - ], - - const SizedBox(width: 4), - - // Multi-Select Toggle (Desktop non-Windows only, icon only, no background, no border) - if (!isMobile && defaultTargetPlatform != TargetPlatform.windows && widget.userRole != 'teacher' && (_tabController.index == 1 || isFolderOpened)) ...[ - Tooltip( - message: _isMultiSelectMode ? 'Exit Multi-Select' : 'Multi-Select', - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.transparent, - side: BorderSide.none, - shadowColor: Colors.transparent, - ), - onPressed: () { - setState(() { - _isMultiSelectMode = !_isMultiSelectMode; - if (!_isMultiSelectMode) _selectedDocumentIds.clear(); - }); - }, - icon: Icon( - Icons.checklist_rounded, - size: 22, - color: _isMultiSelectMode - ? AppColors.primaryGreen - : (isDark ? AppColors.darkTextPrimary : AppColors.textSecondary), + ], ), ), - ), - const SizedBox(width: 4), - ], - - // Filter button (Icon only, between Search and Upload/Bulk Add) - if (_tabController.index == 1 || isFolderOpened) ...[ - Tooltip( - message: 'Filter Documents', - child: IconButton( - onPressed: () => _openFilterDialog( - requirementsAsync, - academicYearsAsync, - statusesAsync, + ] else ...[ + _buildSegmentedTabSwitcher(isDark, isMobile, isMobileOrAndroid), + // Clear student filter chip (compact) + if (isStudentFiltered) ...[ + const SizedBox(width: 8), + Flexible( + child: TextButton.icon( + onPressed: () => ref + .read(documentQueryProvider.notifier) + .setStudentId(null), + icon: const Icon(Icons.close, size: 14), + label: const Text( + 'All Students', + style: TextStyle(fontSize: 12), + ), + style: TextButton.styleFrom( + foregroundColor: AppColors.primaryGreen, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), ), - icon: Badge( - isLabelVisible: _getActiveFilterCount() > 0, - label: Text(_getActiveFilterCount().toString()), - child: Icon( - Icons.tune_rounded, + ], + const Spacer(), + ], + + // Action buttons + if (!isFolderOpened) ...[ + Tooltip( + richMessage: (_searchController.text.isNotEmpty || query.search.isNotEmpty) + ? const TextSpan(text: 'Clear Search') + : const TextSpan( + text: 'Search Documents ', + children: [ + TextSpan( + text: '(Ctrl+F)', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + child: IconButton( + icon: Icon( + (_searchController.text.isNotEmpty || query.search.isNotEmpty) + ? Icons.close + : Icons.search, size: 20, - color: _getActiveFilterCount() > 0 + color: isDark ? AppColors.darkTextPrimary : Colors.black87, + ), + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + onPressed: () { + if (_searchController.text.isNotEmpty || + query.search.isNotEmpty) { + _searchController.clear(); + ref.read(documentQueryProvider.notifier).setSearch(''); + setState(() => _foldersPage = 1); + ref.invalidate(foldersProvider); + ref.invalidate(studentFoldersProvider); + ref.invalidate(documentPageProvider); + } else { + _showSearchDialog(context); + } + }, + ), + ), + ], + + const SizedBox(width: 2), + + // Multi-Select Toggle (Desktop non-Windows only, icon only, no background, no border) + if (!isMobile && defaultTargetPlatform != TargetPlatform.windows && widget.userRole != 'teacher' && (_tabController.index == 1 || isFolderOpened)) ...[ + Tooltip( + message: _isMultiSelectMode ? 'Exit Multi-Select' : 'Multi-Select', + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.transparent, + side: BorderSide.none, + shadowColor: Colors.transparent, + padding: const EdgeInsets.all(8), + ), + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + onPressed: () { + setState(() { + _isMultiSelectMode = !_isMultiSelectMode; + if (!_isMultiSelectMode) _selectedDocumentIds.clear(); + }); + }, + icon: Icon( + Icons.checklist_rounded, + size: 20, + color: _isMultiSelectMode ? AppColors.primaryGreen - : (isDark ? AppColors.darkTextPrimary : Colors.black87), + : (isDark ? AppColors.darkTextPrimary : AppColors.textSecondary), ), ), ), - ), - const SizedBox(width: 4), - ], + const SizedBox(width: 2), + ], - // Desktop action buttons (Upload available to admins and teachers) - if (!isMobile && _tabController.index != 2) ...[ - SizedBox( - height: 36, - child: ElevatedButton.icon( - onPressed: () => UploadOcrModal.show( - context, - prefilledStudentId: - _openedFolderStudentId ?? widget.initialStudentId, - ), - icon: const Icon(Icons.cloud_upload_outlined, size: 16), - label: const Text( - 'Upload', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, + // Filter button (Icon only, between Search and Upload/Bulk Add) + if (_tabController.index == 1 || isFolderOpened) ...[ + Tooltip( + message: 'Filter Documents', + child: IconButton( + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + onPressed: () => _openFilterDialog( + requirementsAsync, + academicYearsAsync, + statusesAsync, ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + icon: Badge( + isLabelVisible: _getActiveFilterCount() > 0, + label: Text(_getActiveFilterCount().toString()), + child: Icon( + Icons.tune_rounded, + size: 20, + color: _getActiveFilterCount() > 0 + ? AppColors.primaryGreen + : (isDark ? AppColors.darkTextPrimary : Colors.black87), + ), ), ), ), - ), - const SizedBox(width: 8), - ], + const SizedBox(width: 2), + ], - if (defaultTargetPlatform != TargetPlatform.android && !isMobile && widget.userRole != 'teacher') ...[ + // Desktop action buttons (Upload available to admins and teachers) + if (!isMobile && _tabController.index != 2) ...[ + SizedBox( + height: 36, + child: ElevatedButton.icon( + onPressed: () => UploadOcrModal.show( + context, + prefilledStudentId: + _openedFolderStudentId ?? widget.initialStudentId, + ), + icon: const Icon(Icons.cloud_upload_outlined, size: 16), + label: const Text( + 'Upload', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryGreen, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + ), + const SizedBox(width: 6), + ], + + if (defaultTargetPlatform != TargetPlatform.android && !isMobile && widget.userRole != 'teacher') ...[ + SizedBox( + height: 36, + child: _buildPrintQueueButton(compact: false), + ), + const SizedBox(width: 6), + ], + + // Dropdown Menu SizedBox( height: 36, - child: _buildPrintQueueButton(compact: false), + width: 36, + child: _buildMoreOptionsDropdown(isMobile), ), - const SizedBox(width: 8), + + if (!isMobile) ...[ + const SizedBox(width: 2), + // Info Button for Download Guide (Desktop) + IconButton( + icon: const Icon( + Icons.info_outline, + color: AppColors.primaryGreen, + size: 20, + ), + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + tooltip: 'Download Guide', + onPressed: () { + showDialog( + context: context, + builder: (_) => const DownloadGuideDialog(), + ); + }, + ), + ], ], - - // Dropdown Menu - SizedBox(height: 38, child: _buildMoreOptionsDropdown(isMobile)), - - const SizedBox(width: 4), - - // Info Button for Download Guide (Moved to right end) - IconButton( - icon: const Icon( - Icons.info_outline, - color: AppColors.primaryGreen, - size: 20, - ), - tooltip: 'Download Guide', - onPressed: () { - showDialog( - context: context, - builder: (_) => const DownloadGuideDialog(), - ); - }, - ), - ], + ), ), ); } @@ -1290,6 +1384,11 @@ class _DocumentsScreenState extends ConsumerState } } else if (value == 'recycle_bin') { showDialog(context: context, builder: (_) => const RecycleBinModal()); + } else if (value == 'download_guide') { + showDialog( + context: context, + builder: (_) => const DownloadGuideDialog(), + ); } }, itemBuilder: (context) => [ @@ -1372,6 +1471,29 @@ class _DocumentsScreenState extends ConsumerState ], ), ), + if (isMobile) ...[ + const PopupMenuDivider(), + PopupMenuItem( + value: 'download_guide', + child: Row( + children: [ + const Icon( + Icons.info_outline, + size: 20, + color: AppColors.primaryGreen, + ), + const SizedBox(width: 8), + Text( + 'Download Guide', + style: TextStyle( + fontSize: 14, + color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + ), + ), + ], + ), + ), + ], ], ); } @@ -2157,13 +2279,20 @@ class _DocumentsScreenState extends ConsumerState decoration: BoxDecoration( color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], + border: isMobile + ? Border.all( + color: isDark ? AppColors.darkBorder : Colors.grey.shade200, + ) + : null, + boxShadow: isMobile + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), @@ -2174,94 +2303,108 @@ class _DocumentsScreenState extends ConsumerState Divider(height: 1, color: isDark ? AppColors.darkBorder : Colors.grey.shade100), itemBuilder: (ctx, i) { final folder = paginatedFolders[i]; - return GestureDetector( - onSecondaryTapDown: widget.userRole == 'teacher' - ? null - : (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - onLongPressStart: widget.userRole == 'teacher' - ? null - : (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - child: InkWell( - onTap: () { - if (folder.studentId != null) { - setState(() { - _openedFolderStudentId = - folder.studentId; - _openedFolderName = folder.name; - }); - ref - .read(documentQueryProvider.notifier) - .setStudentId(folder.studentId); - _searchFocusNode.unfocus(); - _searchController.clear(); - ref - .read(documentQueryProvider.notifier) - .setSearch(''); - } - }, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 16, - vertical: isMobile ? 10 : 12, - ), - child: Row( - children: [ - const Icon( - Icons.folder_rounded, - size: 28, - color: Colors.orange, + return RepaintBoundary( + child: GestureDetector( + onSecondaryTapDown: widget.userRole == 'teacher' + ? null + : (details) => _showFolderContextMenu( + details.globalPosition, + folder, ), - const SizedBox(width: 12), - // Column: name + count files inside + JHS/SHS badges - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - folder.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13, - color: isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary, - ), + onLongPressStart: widget.userRole == 'teacher' + ? null + : (details) => _showFolderContextMenu( + details.globalPosition, + folder, + ), + child: InkWell( + onTap: () { + if (folder.studentId != null) { + setState(() { + _openedFolderStudentId = + folder.studentId; + _openedFolderName = folder.name; + }); + ref + .read(documentQueryProvider.notifier) + .setStudentId(folder.studentId); + _searchFocusNode.unfocus(); + _searchController.clear(); + ref + .read(documentQueryProvider.notifier) + .setSearch(''); + } + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 16, + vertical: isMobile ? 10 : 12, + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.orange.withValues( + alpha: isDark ? 0.16 : 0.10, ), - const SizedBox(height: 2), - Text( - '${folder.documentCount ?? 0} ${folder.documentCount == 1 ? "item" : "items"}', - style: TextStyle( - fontSize: 11, - color: isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary, - ), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Icon( + Icons.folder_rounded, + size: 22, + color: Colors.orange, ), - const SizedBox(height: 4), - _buildFolderCompletionBadge( - folder, - ), - ], + ), ), - ), - Icon( - Icons.chevron_right, - size: 18, - color: isDark - ? AppColors.darkTextMuted - : AppColors.textMuted, - ), - ], + const SizedBox(width: 12), + // Column: name + count files inside + JHS/SHS badges + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + folder.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: isDark + ? AppColors.darkTextPrimary + : AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + '${folder.documentCount ?? 0} ${folder.documentCount == 1 ? "item" : "items"}', + style: TextStyle( + fontSize: 11, + color: isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary, + ), + ), + const SizedBox(height: 4), + _buildFolderCompletionBadge( + folder, + ), + ], + ), + ), + Icon( + Icons.chevron_right, + size: 18, + color: isDark + ? AppColors.darkTextMuted + : AppColors.textMuted, + ), + ], + ), ), ), ), @@ -2300,83 +2443,87 @@ class _DocumentsScreenState extends ConsumerState itemCount: paginatedFolders.length, itemBuilder: (ctx, i) { final folder = paginatedFolders[i]; - return GestureDetector( - onSecondaryTapDown: widget.userRole == 'teacher' - ? null - : (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - onLongPressStart: widget.userRole == 'teacher' - ? null - : (details) => _showFolderContextMenu( - details.globalPosition, - folder, - ), - child: InkWell( - onTap: () { - if (folder.studentId != null) { - setState(() { - _openedFolderStudentId = folder.studentId; - _openedFolderName = folder.name; - }); - ref - .read(documentQueryProvider.notifier) - .setStudentId(folder.studentId); - _searchFocusNode.unfocus(); - _searchController.clear(); - ref.read(documentQueryProvider.notifier).setSearch(''); - } - }, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: isDark ? AppColors.darkBorder : Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.03), - blurRadius: 6, - offset: const Offset(0, 2), + return RepaintBoundary( + child: GestureDetector( + onSecondaryTapDown: widget.userRole == 'teacher' + ? null + : (details) => _showFolderContextMenu( + details.globalPosition, + folder, ), - ], - ), - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.folder_rounded, - size: isMobile ? 38 : 46, - color: Colors.orange, - ), - SizedBox(height: isMobile ? 6 : 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - folder.name, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: isMobile ? 12 : 13, - height: 1.2, - color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + onLongPressStart: widget.userRole == 'teacher' + ? null + : (details) => _showFolderContextMenu( + details.globalPosition, + folder, + ), + child: InkWell( + onTap: () { + if (folder.studentId != null) { + setState(() { + _openedFolderStudentId = folder.studentId; + _openedFolderName = folder.name; + }); + ref + .read(documentQueryProvider.notifier) + .setStudentId(folder.studentId); + _searchFocusNode.unfocus(); + _searchController.clear(); + ref.read(documentQueryProvider.notifier).setSearch(''); + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isDark ? AppColors.darkBorder : Colors.grey.shade200), + boxShadow: isMobile + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.folder_rounded, + size: isMobile ? 38 : 46, + color: Colors.orange, + ), + SizedBox(height: isMobile ? 6 : 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + folder.name, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: isMobile ? 12 : 13, + height: 1.2, + color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary, + ), ), ), - ), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: _buildFolderCompletionBadge(folder), - ), - ], + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: _buildFolderCompletionBadge(folder), + ), + ], + ), ), ), ), @@ -2561,13 +2708,20 @@ class _DocumentsScreenState extends ConsumerState decoration: BoxDecoration( color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], + border: isMobileList + ? Border.all( + color: isDark ? AppColors.darkBorder : Colors.grey.shade200, + ) + : null, + boxShadow: isMobileList + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), diff --git a/frontend/lib/ui/screens/documents/widgets/download_guide_dialog.dart b/frontend/lib/ui/screens/documents/widgets/download_guide_dialog.dart index baac2ba..d748a52 100644 --- a/frontend/lib/ui/screens/documents/widgets/download_guide_dialog.dart +++ b/frontend/lib/ui/screens/documents/widgets/download_guide_dialog.dart @@ -30,7 +30,7 @@ class DownloadGuideDialog extends StatelessWidget { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), + color: Colors.blue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( diff --git a/frontend/lib/ui/screens/documents/widgets/file_folder_card.dart b/frontend/lib/ui/screens/documents/widgets/file_folder_card.dart index fc5cde3..3be6d24 100644 --- a/frontend/lib/ui/screens/documents/widgets/file_folder_card.dart +++ b/frontend/lib/ui/screens/documents/widgets/file_folder_card.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../../core/constants/app_colors.dart'; import '../../../../core/utils/date_utils.dart'; @@ -37,6 +38,10 @@ class FileFolderCard extends StatefulWidget { } class _FileFolderCardState extends State { + static final bool _isMobileOrAndroid = + defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS; + bool get _isExcel => FileIconHelper.isExcel( widget.document.fileName, docType: widget.document.documentType, @@ -217,11 +222,12 @@ class _FileFolderCardState extends State { } // ════════════════════════════════════════ - // GRID CARD — Compact: Filename and Icon only, no chips or 3 dots + // GRID CARD — 2-section design optimized for mobile & desktop // ════════════════════════════════════════ Widget _buildGridCard(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - final baseCardColor = isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite; + final baseCardColor = + isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite; final cardColor = widget.isSelected ? Color.alphaBlend( AppColors.primaryGreen.withValues(alpha: isDark ? 0.22 : 0.12), @@ -229,47 +235,181 @@ class _FileFolderCardState extends State { ) : baseCardColor; - return GestureDetector( - // Desktop: right-click opens context menu - onSecondaryTapDown: (details) => - _showContextMenu(context, details.globalPosition), - // Mobile: long press opens context menu - onLongPressStart: (details) => - _showContextMenu(context, details.globalPosition), - child: Material( - color: cardColor, - borderRadius: BorderRadius.circular(12), - child: InkWell( - onTap: widget.isMultiSelectMode - ? () => widget.onSelectedChanged?.call(!widget.isSelected) - : widget.onTap, + return RepaintBoundary( + child: GestureDetector( + // Desktop: right-click opens context menu + onSecondaryTapDown: (details) => + _showContextMenu(context, details.globalPosition), + // Mobile: long press opens context menu + onLongPressStart: (details) => + _showContextMenu(context, details.globalPosition), + child: Material( + color: cardColor, borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: widget.isSelected - ? AppColors.primaryGreen - : (isDark ? AppColors.darkBorder : Colors.grey.shade200), - width: widget.isSelected ? 1.5 : 1.0, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.03), - blurRadius: 6, - offset: const Offset(0, 2), + child: InkWell( + onTap: widget.isMultiSelectMode + ? () => widget.onSelectedChanged?.call(!widget.isSelected) + : widget.onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: widget.isSelected + ? AppColors.primaryGreen + : (isDark ? AppColors.darkBorder : Colors.grey.shade200), + width: widget.isSelected ? 1.5 : 1.0, ), - ], + boxShadow: _isMobileOrAndroid + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Top section: File preview canvas with icon & quick actions + Expanded( + child: Container( + decoration: BoxDecoration( + color: _fileColor.withValues( + alpha: isDark ? 0.09 : 0.06, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(11), + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Icon(_fileIcon, size: 36, color: _fileColor), + if (widget.isMultiSelectMode) + Positioned( + top: 4, + left: 4, + child: SizedBox( + width: 22, + height: 22, + child: Checkbox( + value: widget.isSelected, + activeColor: AppColors.primaryGreen, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onChanged: widget.onSelectedChanged, + ), + ), + ) + else + Positioned( + top: 2, + right: 2, + child: SizedBox( + width: 26, + height: 26, + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon( + Icons.more_vert, + size: 16, + color: isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary, + ), + onSelected: widget.onActionSelected, + itemBuilder: (_) => _buildMenuItems(), + ), + ), + ), + ], + ), + ), + ), + // Bottom section: File name & metadata + Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 8, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.document.fileName, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 12, + height: 1.25, + color: isDark + ? AppColors.darkTextPrimary + : AppColors.textPrimary, + ), + ), + const SizedBox(height: 3), + Text( + '${FileIconHelper.formatFileSize(widget.document.fileSize ?? widget.document.size)} • ${formatShortDate(widget.document.createdAt)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: isDark + ? AppColors.darkTextMuted + : AppColors.textMuted, + ), + ), + ], + ), + ), + ], + ), ), - child: Stack( - children: [ - if (widget.isMultiSelectMode) - Positioned( - top: 4, - left: 4, - child: SizedBox( - width: 22, - height: 22, + ), + ), + ), + ); + } + + // ════════════════════════════════════════ + // LIST ROW — Premium card row with 40x40 badge & combined metadata + // ════════════════════════════════════════ + Widget _buildListRow(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final baseRowColor = + isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite; + final rowColor = widget.isSelected + ? Color.alphaBlend( + AppColors.primaryGreen.withValues(alpha: isDark ? 0.22 : 0.12), + baseRowColor, + ) + : null; + + return RepaintBoundary( + child: GestureDetector( + onSecondaryTapDown: (details) => + _showContextMenu(context, details.globalPosition), + onLongPressStart: (details) => + _showContextMenu(context, details.globalPosition), + child: Material( + color: rowColor ?? Colors.transparent, + child: InkWell( + onTap: widget.isMultiSelectMode + ? () => widget.onSelectedChanged?.call(!widget.isSelected) + : widget.onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Row( + children: [ + if (widget.isMultiSelectMode) ...[ + SizedBox( + width: 24, + height: 24, child: Checkbox( value: widget.isSelected, activeColor: AppColors.primaryGreen, @@ -280,163 +420,116 @@ class _FileFolderCardState extends State { onChanged: widget.onSelectedChanged, ), ), + const SizedBox(width: 8), + ], + // Premium 40x40 tinted badge + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _fileColor.withValues( + alpha: isDark ? 0.16 : 0.10, + ), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Icon(_fileIcon, size: 22, color: _fileColor), + ), ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon(_fileIcon, size: 40, color: _fileColor), - const SizedBox(height: 6), - Text( - widget.document.fileName, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 12, - height: 1.2, - color: isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary, - ), + const SizedBox(width: 12), + + // File name & Combined metadata subtitle + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.document.fileName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + color: isDark + ? AppColors.darkTextPrimary + : AppColors.textPrimary, ), - ], - ), + ), + const SizedBox(height: 3), + Row( + children: [ + Flexible( + child: Text( + '${FileIconHelper.formatFileSize(widget.document.fileSize ?? widget.document.size)} • ${formatShortDate(widget.document.createdAt)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + color: isDark + ? AppColors.darkTextSecondary + : AppColors.textSecondary, + ), + ), + ), + if (widget.document.documentType != null && + widget.document.documentType!.isNotEmpty) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 1, + ), + decoration: BoxDecoration( + color: isDark + ? Colors.white10 + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: isDark + ? AppColors.darkBorder + : Colors.grey.shade300, + width: 0.5, + ), + ), + child: Text( + widget.document.documentType!, + style: TextStyle( + fontSize: 9.5, + fontWeight: FontWeight.w500, + color: isDark + ? AppColors.darkTextMuted + : AppColors.textMuted, + ), + ), + ), + ], + ], + ), + ], ), ), - ), - ], - ), - ), - ), - ), - ); - } - // ════════════════════════════════════════ - // LIST ROW — Matches folder style: Name, size below, date right, 3 dots - // ════════════════════════════════════════ - Widget _buildListRow(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final baseRowColor = isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite; - final rowColor = widget.isSelected - ? Color.alphaBlend( - AppColors.primaryGreen.withValues(alpha: isDark ? 0.22 : 0.12), - baseRowColor, - ) - : null; - - return GestureDetector( - onSecondaryTapDown: (details) => - _showContextMenu(context, details.globalPosition), - onLongPressStart: (details) => - _showContextMenu(context, details.globalPosition), - child: Material( - color: rowColor ?? Colors.transparent, - child: InkWell( - onTap: widget.isMultiSelectMode - ? () => widget.onSelectedChanged?.call(!widget.isSelected) - : widget.onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - child: Row( - children: [ - if (widget.isMultiSelectMode) ...[ - Checkbox( - value: widget.isSelected, - activeColor: AppColors.primaryGreen, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - onChanged: widget.onSelectedChanged, - ), - const SizedBox(width: 8), - ] else ...[ - const SizedBox(width: 4), - ], - // File icon - SizedBox( - width: 32, - height: 32, - child: Center( - child: Icon(_fileIcon, size: 24, color: _fileColor), - ), - ), - const SizedBox(width: 12), - - // File name & Size below - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.document.fileName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13, - color: isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary, - ), - ), - const SizedBox(height: 2), - Text( - FileIconHelper.formatFileSize( - widget.document.fileSize ?? widget.document.size, - ), - style: TextStyle( - fontSize: 11, + // ⋮ Actions button + if (!widget.isMultiSelectMode) + SizedBox( + width: 32, + height: 32, + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon( + Icons.more_vert, + size: 18, color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, ), + onSelected: widget.onActionSelected, + itemBuilder: (_) => _buildMenuItems(), ), - ], - ), - ), - - const SizedBox(width: 12), - - // Date on right side - Text( - formatShortDate(widget.document.createdAt), - style: TextStyle( - fontSize: 11, - color: isDark - ? AppColors.darkTextMuted - : AppColors.textMuted, - ), - ), - - const SizedBox(width: 4), - - // ⋮ Actions — visible on desktop & mobile - SizedBox( - width: 36, - child: widget.isMultiSelectMode - ? const SizedBox.shrink() - : PopupMenuButton( - icon: Icon( - Icons.more_vert, - size: 18, - color: isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary, - ), - onSelected: widget.onActionSelected, - itemBuilder: (_) => _buildMenuItems(), - ), - ), - ], + ), + ], + ), ), ), ),