feat: [TESIS-68] add the shared modal frame, confirmation dialog and toast - #53
Conversation
…ls onto it ModalFrame is the ModalFrame of the design: header with title, subtitle, optional icon and close, and below it whatever the modal brings (body and footer, or a form wrapping them). It takes the size (sm 480, md 672, lg 880), keeps only the body scrolling, ties the title to aria-labelledby and, with `busy`, cannot be closed from anywhere. It replaces ProductModalShell, which lived in the inventory feature. The create modal can no longer be closed while it is saving, as the edit modal already did with its close button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConfirmDialog asks before running an action, on top of ModalFrame (sm by
default). The destructive tone adds the red warning, paints the confirm button
red and starts with the focus on cancel, so a hurried Enter cannot delete
anything. `canConfirm={false}` leaves only the way out, for when the backend
already refused the action, and `children` carries the reason.
DeleteProductDialog now wraps it, and keeps the last product while the dialog
fades out: the text used to vanish and the box shrank on its way out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cations as toasts The Alert theme override matches Alert.dc.html: tonal background, border in the intent colour, icon and text in the onContainer tone. It applies to the standard and outlined variants, which the app used interchangeably. Only the close glyph goes grey, so actions like "Reintentar" keep the intent colour. The close button is labelled in Spanish. NotificationHost renders each notification as a toast: that same alert, floating, with an opaque background and the level 2 shadow. It no longer closes on a click anywhere on the page; it goes away on its own, with Escape or with its close button. The inventory and product detail pages dropped their own Snackbars and notify through the shared host, like the rest of the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he design system page Alerts in their four intents, with a title and with an action, plus buttons that fire the real toast through notify(). The confirmation dialog shows its standard, destructive and rejected variants in the three sizes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the toast Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TomasMartin2004
left a comment
There was a problem hiding this comment.
Revisión — TESIS-68 (PR #53) · Marco de modal, confirmación destructiva y toasts
Revisado sobre TESIS-68-destructive-confirm-dialog contra origin/master.
| Check | Resultado |
|---|---|
npm run test (local) |
558 tests, 0 fallas |
npm run lint · prettier --check . · npm run build |
Limpios |
✅ Los dos arreglos de comportamiento tienen dientes
Rompí los dos, de a uno, y cayó lo que tenía que caer:
| Rotura | Falla |
|---|---|
Que el toast vuelva a descartarse con clickaway |
stays up when the person clicks elsewhere on the page |
Que el modal se pueda cerrar mientras guarda (onClose sin el guard de busy) |
cannot be closed while something is in flight y does not let anything fire while the action is in flight |
El del toast es el que más se nota en uso real: un aviso que se va con cualquier clic desaparece justo cuando la persona mueve el mouse para seguir trabajando, y el mensaje que importaba —«producto eliminado», «no se pudo guardar»— se pierde sin que nadie lo lea. Que clickaway llegue por el mismo onClose que la X es una trampa de la API de MUI, y está bien resuelta en un solo lugar.
✅ La accesibilidad del diálogo está pensada, no copiada
role="alertdialog"ydescribedByapuntando a la descripción: un lector de pantalla anuncia la pregunta completa, no sólo el título.- El foco arranca en «Cancelar» cuando la acción es destructiva. Es el detalle que evita que un Enter apurado borre un producto, y está explicado en el comentario con esas palabras.
- El botón de confirmar desaparece cuando
canConfirmes falso, en vez de quedar deshabilitado: para una baja que el backend rechazó, un botón gris que no hace nada invita a insistir.
✅ Que sea presentacional es la decisión correcta
ConfirmDialog no ejecuta nada: quien lo monta llama, marca busy y decide si cerrar o dejarlo abierto con el error. Es lo que permite el Caso 2 —la baja rechazada que deja el diálogo abierto con el motivo— sin que el componente sepa nada de productos.
✅ La consolidación era necesaria
El marco de modal vivía dentro de inventory, el único diálogo de confirmación era el de la baja de productos, y dos pantallas tenían su propio Snackbar con otra duración. Mover las tres piezas a shared/components y dejar ProductModalShell atrás es exactamente lo que la card pedía, y el cambio de comportamiento que anotás —el alta ya no se cierra mientras guarda— va en la misma dirección que el de edición.
Me gustó que la re-autenticación para acciones destructivas quede declarada como fuera de alcance con el motivo (el backend no la tiene) en vez de resuelta a medias.
🟡 Menor: NotificationHost quedó duplicado
El PR agrega src/shared/components/NotificationHost/ (carpeta) y en la lista de archivos sigue apareciendo src/shared/components/NotificationHost.tsx (el suelto). Si el viejo quedó en el árbol, conviene borrarlo: dos archivos que resuelven el nombre NotificationHost es la clase de ambigüedad que un import descuidado resuelve para el lado equivocado, y el que sobra no lo cubre ningún test.
Si ya lo borraste y lo que veo es el rename en el diff, ignorá el punto.
Veredicto
APPROVE.
Cierra los componentes estándar de la card reusando lo que ya existía, y los dos arreglos de comportamiento que trae de yapa —el toast que se iba solo y el modal que se cerraba mientras guardaba— están protegidos por ejemplos que fallan si alguien los deshace.
🤖 Generated with Claude Code
|
Ignorá el menor del final: verifiqué en la rama y 🤖 Generated with Claude Code |
|
Aprobado, pero no lo pude mergear: GitHub dice que el merge no se puede crear limpio. Entró TESIS-104 (web#54) mientras tanto y toca las mismas piezas. Simulé el merge en local y son tres archivos: Los tres tienen la misma forma: acá agregás una pieza (el tema del
Un detalle para cuando lo resuelvas: Cuando esté al día lo mergeo. 🤖 Generated with Claude Code |
Brings in TESIS-104 (web#54), which moved the theme to CSS variables and both color schemes. Conflicts: - theme/components/index.ts: keeps the new MuiAlert entry with the mode-less factories of master. - DesignSystemPage.tsx: keeps both imports (notify and useThemeMode). - ProductModalShell.styles.ts: master converted it to theme.vars and this branch deletes it, since ModalFrame replaces it. Kept the deletion. The new styles of this branch (ModalFrame, ConfirmDialog, the toast and the Alert override) now read theme.vars, so the theme toggle repaints them like the rest, with tests that fail against a baked hex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🔗 Link
📝 Descripción
Cierra los componentes estándar de la card: el marco de modal, el modal de confirmación para acciones destructivas y los avisos del sistema como toast, según
ModalFrame.dc.htmlyAlert.dc.html. El formulario de alta que pedía la card ya existía (CreateProductModal, TESIS-63/65), así que se reusa y ahora se monta sobre el marco nuevo.La motivación es que hasta ahora cada pantalla resolvía estas piezas a su manera. El marco de los modales vivía dentro de
inventory. El único diálogo de confirmación era propio de la baja de productos. Los avisos mezclaban el estilo por defecto de MUI y el contorneado. Y dos pantallas tenían unSnackbarpropio, con otro aspecto y otra duración que los del resto de la app.Queda fuera del alcance la "información de autenticación requerida" del modal de borrado: el backend no tiene re-autenticación para acciones destructivas, así que es una card aparte si se quiere.
🛠️ Cambios realizados
ModalFrameenshared/components: cabecera con título, subtítulo, ícono opcional y cierre, y debajoModalBodyyModalFooter, o unModalFormque los envuelva. Tiene tres tamaños (sm480,md672 que es el del diseño,lg880). Solo scrollea el cuerpo, y conbusyno se cierra por ningún lado.ProductModalShell(defeatures/inventory) porModalFrameenCreateProductModalyEditProductModal. Cambio de comportamiento: el modal de alta ya no se puede cerrar mientras guarda (Esc, clic afuera o la X), igual que ya pasaba con la X del de edición.ConfirmDialogsobreModalFrame, ensmpor defecto. Contone="destructive"suma la advertencia roja, pinta el botón de confirmar en rojo y arranca con el foco en Cancelar, así un Enter apurado no borra nada. ConcanConfirm={false}queda solo la salida, para cuando el backend ya rechazó la acción, ychildrenlleva el motivo.DeleteProductDialogpasa a usarConfirmDialogy conserva el último producto mientras el diálogo se desvanece. Antes el texto desaparecía a mitad del cierre.muiAlertal tema (app/theme/components/alert.ts) con el aviso del diseño: fondo tonal, borde del color de la intención, ícono y texto enonContainer. Aplica a las variantesstandardyoutlined. Solo la X va en gris, así que acciones como "Reintentar" conservan el color. La X queda rotulada "Cerrar" (antes decía "Close").NotificationHostpasa a carpeta y muestra cada notificación como toast: el mismo aviso, flotando, con fondo opaco y sombra de nivel 2.clickawayy se descartaba igual que la X. Ahora se va solo a los 6 segundos, con Esc o con su X.Snackbarpropios deInventoryPageyProductDetailPagepornotify(…, 'success')./design-systemla sección "Avisos del sistema y toasts" (avisos en las cuatro intenciones, con título y con acción, y botones que disparan el toast real) y la del modal de confirmación (estándar, destructiva y rechazada, en los tres tamaños).architecture.mdycomponent-structure.md.🧪 Cómo probarlo (Opcional)
Caso 1: baja de un producto
Precondición: sesión iniciada y al menos un producto en el catálogo.
/inventory, abrir el menú de una fila y elegir "Eliminar".→ El diálogo se cierra sin que el texto desaparezca antes, y abajo aparece el toast verde "… eliminado.".
Caso 2: baja rechazada por el backend
Precondición: un producto con ventas o transferencias registradas.
/inventory.→ El diálogo queda abierto con el motivo en un aviso rojo, y el botón "Eliminar" desaparece.
Caso 3: el toast no se va con cualquier clic
/design-system, sección "Avisos del sistema y toasts", tocar cualquier botón de toast.→ El toast sigue visible y se va solo a los 6 segundos, o antes con su X.
Caso 4: modales de producto sobre el marco nuevo
/inventoryy "Editar" en el detalle de un producto.→ Mientras está en vuelo no se cierra. La cabecera y el pie quedan fijos y solo scrollea el cuerpo.
📸 Evidencia (Opcional)
Avisos en línea y toast. A la izquierda,
master: en oscuro los avisos casi no se leen y el toast es un bloque relleno. A la derecha, esta rama.Modal de confirmación (componente nuevo, sin "antes"):
Modal de alta sobre
ModalFrame(cabecera y pie fijos, solo scrollea el cuerpo):¿Afecta la arquitectura o genera un nuevo patrón?
Sí.
ModalFrameyConfirmDialogpasan a ser la forma de armar cualquier modal y cualquier confirmación, y ninguna pantalla monta unSnackbarpropio: todo aviso pasa pornotify(). Está documentado enarchitecture.md. No requiere ADR nuevo: sigue el design system de ADR-007.