e65111a7f4
Permisos (superadmin, viewer, busqueda, tareas recurrentes), bootstrap atomico, revocacion de JWT por version de sesion (028), control optimista de edicion (029), logo sin SVG + CSP, ingesta endurecida, correos escapados, borradores, paginacion de proyectos, accesibilidad de dialogos/controles e i18n. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48 lines
2.0 KiB
React
48 lines
2.0 KiB
React
import { createPortal } from 'react-dom';
|
|
import { useUi } from '../context/ui-context.jsx';
|
|
import { Button } from './Button.jsx';
|
|
import { useId } from 'react';
|
|
import { useModalFocus } from '../hooks/useModalFocus.js';
|
|
|
|
const STRIPE_COLOR = { info: 'accent', success: 'success', warning: 'warning', error: 'danger' };
|
|
|
|
// ContentDialog global — equivalente al AppDialog de la identidad web. Contrato:
|
|
// openDialog({ tone, title, message, actions: [{ label, variant, keepOpen, onClick }] })
|
|
export function ContentDialogHost() {
|
|
const { dialog, closeDialog, t } = useUi();
|
|
const titleId = useId();
|
|
const modalRef = useModalFocus(!!dialog);
|
|
if (!dialog) return null;
|
|
|
|
const tone = dialog.tone || 'info';
|
|
const actions = dialog.actions?.length ? dialog.actions : [{ label: t('common_close'), variant: 'accent' }];
|
|
|
|
async function handleClick(action) {
|
|
if (action.onClick) await action.onClick();
|
|
if (!action.keepOpen) closeDialog();
|
|
}
|
|
|
|
// Regla dura: NUNCA se cierra con clic afuera (a diferencia de Flyout) — solo con un boton.
|
|
// Portal a document.body (igual que Flyout/DetailPopup): garantiza que quede SIEMPRE por
|
|
// encima de cualquier DetailPopup abierto, sin depender del punto del arbol donde se monta.
|
|
return createPortal(
|
|
<div className="dialog-overlay">
|
|
<div className="dialog-surface" role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1} ref={modalRef}>
|
|
<div className="dialog-stripe" style={{ background: `rgb(var(--${STRIPE_COLOR[tone]}))` }} />
|
|
<div className="dialog-body">
|
|
<div className="dialog-title" id={titleId}>{dialog.title}</div>
|
|
<div className="dialog-message">{dialog.message}</div>
|
|
</div>
|
|
<div className="dialog-actions">
|
|
{actions.map((action, i) => (
|
|
<Button key={i} variant={action.variant || 'standard'} onClick={() => handleClick(action)}>
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
);
|
|
}
|