From b0abd68c2e771e98b76a982b6fa04d68f7b1ed66 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Mar 2026 20:35:22 -0500 Subject: [PATCH] feat: Implement certificate upload form with metadata fields, expiry date selection, and file upload functionality --- .../pages/certificate_upload_page.dart | 342 +++--------------- .../certificate_metadata_fields.dart | 79 ++++ .../certificate_upload_actions.dart | 101 ++++++ .../expiry_date_field.dart | 60 +++ .../file_selector.dart | 75 ++++ .../certificate_upload_page/index.dart | 5 + .../pdf_file_types_banner.dart | 35 ++ 7 files changed, 400 insertions(+), 297 deletions(-) create mode 100644 apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_metadata_fields.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_upload_actions.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/expiry_date_field.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/file_selector.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/index.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/pdf_file_types_banner.dart diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart index 74ec77f3..d8dd6456 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/pages/certificate_upload_page.dart @@ -1,17 +1,17 @@ import 'dart:io'; +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; -import 'package:core_localization/core_localization.dart'; -import 'package:intl/intl.dart'; +import '../../domain/usecases/upload_certificate_usecase.dart'; import '../blocs/certificate_upload/certificate_upload_cubit.dart'; import '../blocs/certificate_upload/certificate_upload_state.dart'; -import '../../domain/usecases/upload_certificate_usecase.dart'; +import '../widgets/certificate_upload_page/index.dart'; /// Page for uploading a certificate with metadata (expiry, issuer, etc). class CertificateUploadPage extends StatefulWidget { @@ -179,62 +179,16 @@ class _CertificateUploadPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _PdfFileTypesBanner( + PdfFileTypesBanner( message: t.staff_documents.upload.pdf_banner, ), const SizedBox(height: UiConstants.space6), - // Name Field - Text( - t.staff_certificates.upload_modal.name_label, - style: UiTypography.body2m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - TextField( - controller: _nameController, - enabled: _isNewCertificate, - decoration: InputDecoration( - hintText: t.staff_certificates.upload_modal.name_hint, - border: OutlineInputBorder( - borderRadius: UiConstants.radiusLg, - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - // Issuer Field - Text( - t.staff_certificates.upload_modal.issuer_label, - style: UiTypography.body2m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - TextField( - controller: _issuerController, - enabled: _isNewCertificate, - decoration: InputDecoration( - hintText: t.staff_certificates.upload_modal.issuer_hint, - border: OutlineInputBorder( - borderRadius: UiConstants.radiusLg, - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - // Certificate Number Field - Text( - 'Certificate Number', - style: UiTypography.body2m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - TextField( - controller: _numberController, - enabled: _isNewCertificate, - decoration: InputDecoration( - hintText: 'Enter number if applicable', - border: OutlineInputBorder( - borderRadius: UiConstants.radiusLg, - ), - ), + CertificateMetadataFields( + nameController: _nameController, + issuerController: _issuerController, + numberController: _numberController, + isNewCertificate: _isNewCertificate, ), const SizedBox(height: UiConstants.space6), @@ -242,44 +196,9 @@ class _CertificateUploadPageState extends State { const SizedBox(height: UiConstants.space6), - // Expiry Date Field - Text( - t.staff_certificates.upload_modal.expiry_label, - style: UiTypography.body2m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - InkWell( + ExpiryDateField( + selectedDate: _selectedExpiryDate, onTap: _selectDate, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - decoration: BoxDecoration( - border: Border.all(color: UiColors.border), - borderRadius: UiConstants.radiusLg, - ), - child: Row( - children: [ - const Icon( - UiIcons.calendar, - size: 20, - color: UiColors.textSecondary, - ), - const SizedBox(width: UiConstants.space3), - Text( - _selectedExpiryDate != null - ? DateFormat( - 'MMM dd, yyyy', - ).format(_selectedExpiryDate!) - : t.staff_certificates.upload_modal.select_date, - style: _selectedExpiryDate != null - ? UiTypography.body1m.textPrimary - : UiTypography.body1m.textSecondary, - ), - ], - ), - ), ), const SizedBox(height: UiConstants.space4), @@ -289,7 +208,7 @@ class _CertificateUploadPageState extends State { style: UiTypography.body2m.textPrimary, ), const SizedBox(height: UiConstants.space2), - _FileSelector( + FileSelector( selectedFilePath: _selectedFilePath, onTap: _pickFile, ), @@ -299,110 +218,40 @@ class _CertificateUploadPageState extends State { bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: UiConstants.space4, - children: [ - // Attestation - Row( - children: [ - Checkbox( - value: state.isAttested, - onChanged: (bool? val) => - BlocProvider.of( - context, - ).setAttested(val ?? false), - activeColor: UiColors.primary, - ), - Expanded( - child: Text( - t.staff_documents.upload.attestation, - style: UiTypography.body3r.textSecondary, - ), - ), - ], - ), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: - (_selectedFilePath != null && - state.isAttested && - _nameController.text.isNotEmpty) - ? () { - final String? err = _validatePdfFile( - context, - _selectedFilePath!, - ); - if (err != null) { - UiSnackbar.show( - context, - message: err, - type: UiSnackbarType.error, - margin: const EdgeInsets.all( - UiConstants.space4, - ), - ); - return; - } - BlocProvider.of( - context, - ).uploadCertificate( - UploadCertificateParams( - certificationType: _selectedType!, - name: _nameController.text, - filePath: _selectedFilePath!, - expiryDate: _selectedExpiryDate, - issuer: _issuerController.text, - certificateNumber: _numberController.text, - ), - ); - } - : null, - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusLg, - ), - ), - child: state.status == CertificateUploadStatus.uploading - ? const CircularProgressIndicator( - color: Colors.white, - ) - : Text( - t.staff_certificates.upload_modal.save, - style: UiTypography.body1m.white, - ), + child: CertificateUploadActions( + isAttested: state.isAttested, + isFormValid: _selectedFilePath != null && + state.isAttested && + _nameController.text.isNotEmpty, + isUploading: state.status == CertificateUploadStatus.uploading, + hasExistingCertificate: widget.certificate != null, + onUploadPressed: () { + final String? err = _validatePdfFile( + context, + _selectedFilePath!, + ); + if (err != null) { + UiSnackbar.show( + context, + message: err, + type: UiSnackbarType.error, + margin: const EdgeInsets.all(UiConstants.space4), + ); + return; + } + BlocProvider.of(context) + .uploadCertificate( + UploadCertificateParams( + certificationType: _selectedType!, + name: _nameController.text, + filePath: _selectedFilePath!, + expiryDate: _selectedExpiryDate, + issuer: _issuerController.text, + certificateNumber: _numberController.text, ), - ), - - // Remove Button (only if existing) - if (widget.certificate != null) ...[ - SizedBox( - width: double.infinity, - child: TextButton.icon( - onPressed: () => _showRemoveConfirmation(context), - icon: const Icon(UiIcons.delete, size: 20), - label: Text(t.staff_certificates.card.remove), - style: TextButton.styleFrom( - foregroundColor: UiColors.destructive, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusLg, - side: const BorderSide( - color: UiColors.destructive, - ), - ), - ), - ), - ), - ], - ], + ); + }, + onRemovePressed: () => _showRemoveConfirmation(context), ), ), ), @@ -412,104 +261,3 @@ class _CertificateUploadPageState extends State { ); } } - -/// Banner displaying accepted file types and size limit for PDF upload. -class _PdfFileTypesBanner extends StatelessWidget { - const _PdfFileTypesBanner({required this.message}); - - final String message; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - decoration: BoxDecoration( - color: UiColors.primaryForeground, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(UiIcons.info, size: 20, color: UiColors.primary), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Text(message, style: UiTypography.body2r.textSecondary), - ), - ], - ), - ); - } -} - -class _FileSelector extends StatelessWidget { - const _FileSelector({this.selectedFilePath, required this.onTap}); - - final String? selectedFilePath; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - if (selectedFilePath != null) { - return InkWell( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - border: Border.all(color: UiColors.primary), - borderRadius: UiConstants.radiusLg, - ), - child: Row( - children: [ - const Icon(UiIcons.certificate, color: UiColors.primary), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Text( - selectedFilePath!.split('/').last, - style: UiTypography.body1m.primary, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - t.staff_documents.upload.replace, - style: UiTypography.body3m.primary, - ), - ], - ), - ), - ); - } - - return InkWell( - onTap: onTap, - child: Container( - height: 120, - width: double.infinity, - decoration: BoxDecoration( - border: Border.all(color: UiColors.border, style: BorderStyle.solid), - borderRadius: UiConstants.radiusLg, - color: UiColors.background, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(UiIcons.uploadCloud, size: 32, color: UiColors.primary), - const SizedBox(height: UiConstants.space2), - Text( - t.staff_certificates.upload_modal.drag_drop, - style: UiTypography.body2m, - ), - Text( - t.staff_certificates.upload_modal.supported_formats, - style: UiTypography.body3r.textSecondary, - ), - ], - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_metadata_fields.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_metadata_fields.dart new file mode 100644 index 00000000..e8849918 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_metadata_fields.dart @@ -0,0 +1,79 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:core_localization/core_localization.dart'; + +/// Widget for certificate metadata input fields (name, issuer, number). +class CertificateMetadataFields extends StatelessWidget { + const CertificateMetadataFields({ + required this.nameController, + required this.issuerController, + required this.numberController, + required this.isNewCertificate, + }); + + final TextEditingController nameController; + final TextEditingController issuerController; + final TextEditingController numberController; + final bool isNewCertificate; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name Field + Text( + t.staff_certificates.upload_modal.name_label, + style: UiTypography.body2m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), + TextField( + controller: nameController, + enabled: isNewCertificate, + decoration: InputDecoration( + hintText: t.staff_certificates.upload_modal.name_hint, + border: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + ), + ), + ), + const SizedBox(height: UiConstants.space4), + + // Issuer Field + Text( + t.staff_certificates.upload_modal.issuer_label, + style: UiTypography.body2m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), + TextField( + controller: issuerController, + enabled: isNewCertificate, + decoration: InputDecoration( + hintText: t.staff_certificates.upload_modal.issuer_hint, + border: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + ), + ), + ), + const SizedBox(height: UiConstants.space4), + + // Certificate Number Field + Text( + 'Certificate Number', + style: UiTypography.body2m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), + TextField( + controller: numberController, + enabled: isNewCertificate, + decoration: InputDecoration( + hintText: 'Enter number if applicable', + border: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_upload_actions.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_upload_actions.dart new file mode 100644 index 00000000..3887e5df --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/certificate_upload_actions.dart @@ -0,0 +1,101 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:core_localization/core_localization.dart'; + +import '../../blocs/certificate_upload/certificate_upload_cubit.dart'; + +/// Widget for attestation checkbox and action buttons in certificate upload form. +class CertificateUploadActions extends StatelessWidget { + const CertificateUploadActions({ + required this.isAttested, + required this.isFormValid, + required this.isUploading, + required this.hasExistingCertificate, + required this.onUploadPressed, + required this.onRemovePressed, + }); + + final bool isAttested; + final bool isFormValid; + final bool isUploading; + final bool hasExistingCertificate; + final VoidCallback onUploadPressed; + final VoidCallback onRemovePressed; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + spacing: UiConstants.space4, + children: [ + // Attestation + Row( + children: [ + Checkbox( + value: isAttested, + onChanged: (bool? val) => + BlocProvider.of(context).setAttested( + val ?? false, + ), + activeColor: UiColors.primary, + ), + Expanded( + child: Text( + t.staff_documents.upload.attestation, + style: UiTypography.body3r.textSecondary, + ), + ), + ], + ), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isFormValid ? onUploadPressed : null, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusLg, + ), + ), + child: isUploading + ? const CircularProgressIndicator( + color: Colors.white, + ) + : Text( + t.staff_certificates.upload_modal.save, + style: UiTypography.body1m.white, + ), + ), + ), + + // Remove Button (only if existing) + if (hasExistingCertificate) ...[ + SizedBox( + width: double.infinity, + child: TextButton.icon( + onPressed: onRemovePressed, + icon: const Icon(UiIcons.delete, size: 20), + label: Text(t.staff_certificates.card.remove), + style: TextButton.styleFrom( + foregroundColor: UiColors.destructive, + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusLg, + side: const BorderSide( + color: UiColors.destructive, + ), + ), + ), + ), + ), + ], + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/expiry_date_field.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/expiry_date_field.dart new file mode 100644 index 00000000..43fd484d --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/expiry_date_field.dart @@ -0,0 +1,60 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:core_localization/core_localization.dart'; + +/// Widget for selecting certificate expiry date. +class ExpiryDateField extends StatelessWidget { + const ExpiryDateField({ + required this.selectedDate, + required this.onTap, + }); + + final DateTime? selectedDate; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.staff_certificates.upload_modal.expiry_label, + style: UiTypography.body2m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), + InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + ), + child: Row( + children: [ + const Icon( + UiIcons.calendar, + size: 20, + color: UiColors.textSecondary, + ), + const SizedBox(width: UiConstants.space3), + Text( + selectedDate != null + ? DateFormat('MMM dd, yyyy').format(selectedDate!) + : t.staff_certificates.upload_modal.select_date, + style: selectedDate != null + ? UiTypography.body1m.textPrimary + : UiTypography.body1m.textSecondary, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/file_selector.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/file_selector.dart new file mode 100644 index 00000000..a770779d --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/file_selector.dart @@ -0,0 +1,75 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:core_localization/core_localization.dart'; + +/// Widget for selecting certificate file. +class FileSelector extends StatelessWidget { + const FileSelector({ + required this.selectedFilePath, + required this.onTap, + }); + + final String? selectedFilePath; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + if (selectedFilePath != null) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + border: Border.all(color: UiColors.primary), + borderRadius: UiConstants.radiusLg, + ), + child: Row( + children: [ + const Icon(UiIcons.certificate, color: UiColors.primary), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Text( + selectedFilePath!.split('/').last, + style: UiTypography.body1m.primary, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + t.staff_documents.upload.replace, + style: UiTypography.body3m.primary, + ), + ], + ), + ), + ); + } + + return InkWell( + onTap: onTap, + child: Container( + height: 120, + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: UiColors.border, style: BorderStyle.solid), + borderRadius: UiConstants.radiusLg, + color: UiColors.background, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(UiIcons.uploadCloud, size: 32, color: UiColors.primary), + const SizedBox(height: UiConstants.space2), + Text( + t.staff_certificates.upload_modal.drag_drop, + style: UiTypography.body2m, + ), + Text( + t.staff_certificates.upload_modal.supported_formats, + style: UiTypography.body3r.textSecondary, + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/index.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/index.dart new file mode 100644 index 00000000..bda59ee4 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/index.dart @@ -0,0 +1,5 @@ +export 'certificate_metadata_fields.dart'; +export 'certificate_upload_actions.dart'; +export 'expiry_date_field.dart'; +export 'file_selector.dart'; +export 'pdf_file_types_banner.dart'; diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/pdf_file_types_banner.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/pdf_file_types_banner.dart new file mode 100644 index 00000000..5c5252df --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/presentation/widgets/certificate_upload_page/pdf_file_types_banner.dart @@ -0,0 +1,35 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// Banner displaying accepted file types and size limit for PDF upload. +class PdfFileTypesBanner extends StatelessWidget { + const PdfFileTypesBanner({required this.message}); + + final String message; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + color: UiColors.primaryForeground, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(UiIcons.info, size: 20, color: UiColors.primary), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Text(message, style: UiTypography.body2r.textSecondary), + ), + ], + ), + ); + } +}