diff --git a/lib/core/services/materials_rag_service.dart b/lib/core/services/materials_rag_service.dart index 5591f20..578c5f9 100644 --- a/lib/core/services/materials_rag_service.dart +++ b/lib/core/services/materials_rag_service.dart @@ -558,6 +558,76 @@ class MaterialsRAGService { return scoredChunks.take(maxChunks).map((e) => e.key).toList(); } + /// Formatar texto extraído do PDF para melhor legibilidade + static String _formatPDFText(String text) { + if (text.isEmpty) return text; + + String formatted = text; + + // Corrigir quebras de linha excessivas + formatted = formatted.replaceAll(RegExp(r'\n{3,}'), '\n\n'); + + // Corrigir espaços excessivos + formatted = formatted.replaceAll(RegExp(r'[ \t]+'), ' '); + + // Remover espaços no início/fim das linhas + formatted = formatted.split('\n').map((line) => line.trim()).join('\n'); + + // Corrigir parágrafos (linhas que terminam com ponto e seguem sem espaço) + formatted = formatted.replaceAllMapped( + RegExp(r'\.(\n)([A-ZÁÉÍÓÚÀÂÊÔÃÕÇ])'), + (match) => '.\n\n${match.group(2)}', + ); + + // Corrigir quebras de palavras com hífen no fim da linha + formatted = formatted.replaceAllMapped( + RegExp(r'([a-zA-Záéíóúàâêôãõç])-\n([a-zA-Záéíóúàâêôãõç])'), + (match) => '${match.group(1)}${match.group(2)}', + ); + + // Adicionar quebras de parágrafo para títulos (linhas em maiúsculas) + formatted = formatted.replaceAllMapped( + RegExp(r'\n([A-ZÁÉÍÓÚÀÂÊÔÃÕÇ][A-ZÁÉÍÓÚÀÂÊÔÃÕÇ\s]{10,})\n'), + (match) => '\n\n${match.group(1)}\n\n', + ); + + // Limpar quebras de linha no início e fim + formatted = formatted.trim(); + + return formatted; + } + + /// Obter o texto completo de um PDF específico para pré-visualização + static Future getFullPDFText(String fileName, String teacherId) async { + try { + // Remover extensão se existir + final cleanFileName = fileName.endsWith('.pdf') ? fileName : '$fileName.pdf'; + + // Usar cache do texto completo se disponível + final cacheKey = '${cleanFileName}_preview_v6'; + if (_chunksCache.containsKey(cacheKey) && _chunksCache[cacheKey]!.isNotEmpty) { + final fullText = _chunksCache[cacheKey]!.first; + Logger.info('Using cached preview text for $cleanFileName: ${fullText.length} chars'); + return fullText; + } + + // Extrair texto completo + final rawText = await _extractFullText(cleanFileName, teacherId); + + // Formatar texto para melhor legibilidade + final formattedText = _formatPDFText(rawText); + + // Guardar em cache + _chunksCache[cacheKey] = [formattedText]; + + Logger.info('PDF "$cleanFileName" -> ${formattedText.length} chars extracted and formatted for preview'); + return formattedText; + } catch (e) { + Logger.error('Error getting full PDF text for $fileName: $e'); + return ''; + } + } + /// Clear the chunks cache static void clearCache() { _chunksCache.clear(); diff --git a/lib/features/quiz/presentation/pages/quiz_list_page.dart b/lib/features/quiz/presentation/pages/quiz_list_page.dart index 037e7b3..1c43b1a 100644 --- a/lib/features/quiz/presentation/pages/quiz_list_page.dart +++ b/lib/features/quiz/presentation/pages/quiz_list_page.dart @@ -648,6 +648,324 @@ class _QuizListPageState extends State dynamic _jsonDecode(String s) => jsonDecode(s); + Future _previewPDF(Map mat, String name) async { + final matId = mat['id']!; + final matName = name.replaceAll('.pdf', '').replaceAll('_', ' '); + + // Mostrar loading + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + content: Row( + children: [ + CircularProgressIndicator(), + const SizedBox(width: 16), + Text('A carregar PDF...'), + ], + ), + ), + ); + + try { + // Obter o texto completo do PDF usando o método existente + final teacherId = mat['teacherId']; + if (teacherId == null) { + Navigator.of(context).pop(); + _showSnack('Erro: não foi possível identificar o professor do material.'); + return; + } + + final fullText = await MaterialsRAGService.getFullPDFText(matName, teacherId); + + Navigator.of(context).pop(); // Fechar loading + + if (fullText.isEmpty) { + _showSnack('Não foi possível carregar o conteúdo do PDF.'); + return; + } + + // Mostrar o conteúdo em um diálogo scrollável melhorado + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + height: MediaQuery.of(context).size.height * 0.90, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, -4), + ), + ], + ), + child: Column( + children: [ + // Handle bar + Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4), + borderRadius: BorderRadius.circular(2), + ), + ), + + // Header melhorado + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.primary.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.3)), + ), + child: Icon(Icons.picture_as_pdf, color: Colors.white, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + matName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + 'Pré-visualização do conteúdo completo', + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(0.9), + ), + ), + ], + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white, size: 20), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Stats bar + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Row( + children: [ + Icon(Icons.description, color: Colors.white.withOpacity(0.9), size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + '${(fullText.length / 1000).toStringAsFixed(1)}K caracteres • ${fullText.split('\n').length} linhas', + style: TextStyle( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'PDF', + style: TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + // Content area melhorado + Expanded( + child: Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.1), + ), + ), + child: Column( + children: [ + // Content header + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Row( + children: [ + Icon(Icons.text_fields, + color: Theme.of(context).colorScheme.primary, + size: 18), + const SizedBox(width: 8), + Text( + 'Conteúdo do Material', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Selecionável', + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + + // Text content + Expanded( + child: Container( + padding: const EdgeInsets.all(20), + child: SingleChildScrollView( + child: SelectableText( + fullText, + style: TextStyle( + fontSize: 14, + height: 1.7, + color: Theme.of(context).colorScheme.onSurface, + fontFamily: 'Roboto', + letterSpacing: 0.3, + ), + ), + ), + ), + ), + ], + ), + ), + ), + + // Footer melhorado + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.info_outline, + color: Theme.of(context).colorScheme.primary, + size: 16), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Texto extraído automaticamente do PDF', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + 'Formatação otimizada para melhor legibilidade', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.8), + ), + ), + ], + ), + ), + Icon(Icons.keyboard_arrow_up, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 16), + ], + ), + ), + ], + ), + ), + ); + + } catch (e) { + Logger.error('Error previewing PDF: $e'); + Navigator.of(context).pop(); // Fechar loading + _showSnack('Erro ao carregar o PDF. Tenta novamente.'); + } + } + void _showInteractiveQuiz( String title, List<_QuizQuestion> questions, { @@ -1030,55 +1348,587 @@ class _QuizListPageState extends State bool isGenerating, ColorScheme cs, ) { + final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' '); + return Container( + margin: const EdgeInsets.only(bottom: 4), decoration: BoxDecoration( color: cs.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: cs.outline.withOpacity(0.15)), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: cs.outline.withOpacity(0.08), + width: 1, + ), boxShadow: [ BoxShadow( - color: cs.shadow.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, 2), + color: cs.shadow.withOpacity(0.04), + blurRadius: 12, + offset: const Offset(0, 4), + ), + BoxShadow( + color: cs.primary.withOpacity(0.03), + blurRadius: 20, + offset: const Offset(0, 8), ), ], ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: cs.secondary.withOpacity(0.12), - borderRadius: BorderRadius.circular(10), - ), - child: Icon(Icons.picture_as_pdf, color: cs.secondary, size: 22), - ), - title: Text( - name.replaceAll('.pdf', '').replaceAll('_', ' '), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: cs.onSurface, - ), - ), - subtitle: Text( - 'Toca para gerar um quiz', - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), - ), - trailing: isGenerating - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2.5, - color: cs.primary, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: isGenerating ? null : () => _showMaterialOptions(mat, name, cs), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // PDF Icon com design melhorado + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + cs.secondary.withOpacity(0.15), + cs.secondary.withOpacity(0.08), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: cs.secondary.withOpacity(0.2), + width: 1, + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.picture_as_pdf, + color: cs.secondary, + size: 26, + ), + if (!isGenerating) + Positioned( + bottom: 2, + right: 2, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.play_arrow, + color: Colors.white, + size: 10, + ), + ), + ), + ], + ), ), - ) - : Icon(Icons.play_circle_outline, color: cs.primary, size: 28), - onTap: isGenerating ? null : () => _generateQuiz(mat), + + const SizedBox(width: 16), + + // Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + cleanName, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 15, + color: cs.onSurface, + letterSpacing: 0.2, + ), + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: cs.primaryContainer.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.touch_app, + size: 12, + color: cs.primary, + ), + const SizedBox(width: 4), + Text( + isGenerating ? 'A gerar...' : 'Toca para opções', + style: TextStyle( + fontSize: 11, + color: cs.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + + // Action indicator + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isGenerating + ? cs.primary.withOpacity(0.1) + : cs.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isGenerating + ? cs.primary.withOpacity(0.2) + : cs.outline.withOpacity(0.1), + ), + ), + child: isGenerating + ? Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: cs.primary, + ), + ), + ) + : Icon( + Icons.more_vert, + color: cs.onSurfaceVariant, + size: 20, + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _showMaterialOptions(Map mat, String name, ColorScheme cs) { + final cleanName = name.replaceAll('.pdf', '').replaceAll('_', ' '); + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) => Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 25, + offset: const Offset(0, -5), + ), + ], + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle bar melhorado + Container( + width: 48, + height: 5, + margin: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: cs.onSurfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(3), + ), + ), + + // Header melhorado com gradient + Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + cs.primary.withOpacity(0.05), + cs.secondary.withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: cs.outline.withOpacity(0.1), + ), + ), + child: Row( + children: [ + // PDF Icon animado + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + cs.primary.withOpacity(0.2), + cs.secondary.withOpacity(0.15), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: cs.primary.withOpacity(0.3), + width: 1.5, + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.picture_as_pdf, + color: cs.primary, + size: 28, + ), + Positioned( + top: 4, + right: 4, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: cs.secondary, + shape: BoxShape.circle, + ), + child: Icon( + Icons.star, + color: Colors.white, + size: 8, + ), + ), + ), + ], + ), + ), + + const SizedBox(width: 16), + + // Title section + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + cleanName, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: cs.onSurface, + letterSpacing: 0.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: cs.primaryContainer.withOpacity(0.6), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'Material de estudo', + style: TextStyle( + fontSize: 11, + color: cs.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Instructions + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Icon( + Icons.lightbulb_outline, + color: cs.tertiary, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'O que gostarias de fazer com este material?', + style: TextStyle( + fontSize: 15, + color: cs.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Options melhoradas + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + // Preview PDF option com design premium + _buildPremiumOptionTile( + icon: Icons.visibility_rounded, + title: 'Pré-visualizar PDF', + subtitle: 'Ler o conteúdo completo do material', + description: 'Texto formatado e otimizado para leitura', + color: cs.primary, + gradient: LinearGradient( + colors: [ + cs.primary.withOpacity(0.1), + cs.primary.withOpacity(0.05), + ], + ), + onTap: () { + Navigator.of(context).pop(); + _previewPDF(mat, name); + }, + ), + + const SizedBox(height: 16), + + // Generate Quiz option com design premium + _buildPremiumOptionTile( + icon: Icons.quiz_rounded, + title: 'Gerar Quiz', + subtitle: 'Criar um quiz personalizado', + description: 'Baseado em IA com perguntas inteligentes', + color: cs.secondary, + gradient: LinearGradient( + colors: [ + cs.secondary.withOpacity(0.1), + cs.secondary.withOpacity(0.05), + ], + ), + onTap: () { + Navigator.of(context).pop(); + _generateQuiz(mat); + }, + ), + ], + ), + ), + + const SizedBox(height: 32), + ], + ), + ), + ), + ); + } + + Widget _buildPremiumOptionTile({ + required IconData icon, + required String title, + required String subtitle, + required String description, + required Color color, + required Gradient gradient, + required VoidCallback onTap, + }) { + final cs = Theme.of(context).colorScheme; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: color.withOpacity(0.2), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // Icon container premium + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: color.withOpacity(0.3), + width: 1, + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Icon(icon, color: color, size: 28), + Positioned( + bottom: 2, + right: 2, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Icon( + Icons.arrow_forward, + color: Colors.white, + size: 8, + ), + ), + ), + ], + ), + ), + + const SizedBox(width: 16), + + // Text content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: cs.onSurface, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + fontSize: 13, + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: TextStyle( + fontSize: 11, + color: cs.onSurfaceVariant.withOpacity(0.8), + ), + ), + ], + ), + ), + + // Arrow indicator + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.chevron_right, + color: color, + size: 18, + ), + ), + ], + ), + ), + ); + } + + Widget _buildOptionTile({ + required IconData icon, + required String title, + required String subtitle, + required Color color, + required VoidCallback onTap, + }) { + final cs = Theme.of(context).colorScheme; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.2)), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon(Icons.chevron_right, color: cs.onSurfaceVariant, size: 20), + ], + ), ), ); }