Files

180 lines
5.4 KiB
Dart
Raw Permalink Normal View History

2026-09-21 16:33:51 -04:00
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!,
],
),
),
),
);
}
}