first commit
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
/// Función estándar para abrir modales médicos en Hidalyx Med.
|
||||
///
|
||||
/// Cumple con la regla institucional estricta:
|
||||
/// - barrierDismissible: false (NO se cierra haciendo clic en el exterior).
|
||||
/// - Solamente se cierra mediante la tecla ESC o el botón (X) superior derecho.
|
||||
Future<T?> showMedicalModal<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
bool useRootNavigator = true,
|
||||
}) {
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||||
useRootNavigator: useRootNavigator,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
|
||||
/// Contenedor base estándar para todos los modales de Hidalyx Med.
|
||||
///
|
||||
/// Implementa:
|
||||
/// 1. Cierre garantizado con tecla [ESC] a nivel hardware y de atajos.
|
||||
/// 2. Botón con icono de cerrar (X) en la esquina superior derecha.
|
||||
/// 3. Bloqueo de cierre al tocar fuera del diálogo.
|
||||
class MedicalModalDialog extends StatefulWidget {
|
||||
final Widget? titleIcon;
|
||||
final String title;
|
||||
final Widget? subtitle;
|
||||
final Widget child;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
final VoidCallback? onClose;
|
||||
final Widget? bottomBar;
|
||||
final List<Widget>? actions;
|
||||
|
||||
const MedicalModalDialog({
|
||||
super.key,
|
||||
this.titleIcon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.child,
|
||||
this.maxWidth = 740,
|
||||
this.maxHeight = 620,
|
||||
this.onClose,
|
||||
this.bottomBar,
|
||||
this.actions,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MedicalModalDialog> createState() => _MedicalModalDialogState();
|
||||
}
|
||||
|
||||
class _MedicalModalDialogState extends State<MedicalModalDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
HardwareKeyboard.instance.addHandler(_handleHardwareKey);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleHardwareKey);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _handleHardwareKey(KeyEvent event) {
|
||||
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
_close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _close() {
|
||||
if (!mounted) return;
|
||||
if (widget.onClose != null) {
|
||||
widget.onClose!();
|
||||
} else {
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.maxWidth,
|
||||
maxHeight: widget.maxHeight,
|
||||
minWidth: 320,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
blurRadius: 28,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Encabezado Superior con Botón (X) de Cierre
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: AppColors.border),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.titleIcon != null) ...[
|
||||
widget.titleIcon!,
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
if (widget.subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
widget.subtitle!,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.actions != null) ...[
|
||||
...widget.actions!,
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
// Botón oficial de cerrar (X) en esquina superior derecha
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close_rounded,
|
||||
color: AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Cerrar (Esc)',
|
||||
splashRadius: 20,
|
||||
onPressed: _close,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Contenido Principal
|
||||
Expanded(child: widget.child),
|
||||
|
||||
// Barra Inferior Informativa (Opcional)
|
||||
if (widget.bottomBar != null) widget.bottomBar!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user