metis-desk/docs/patron-de-dialogos.md
David ddebfadc1c feat: add contextual help components and dialog support
- Introduced `ContextHelpContent` for displaying contextual help with title, description, examples, and guidance.
- Created `DialogHelp` component to manage help dialogs, including popover and panel presentations.
- Implemented `useDialogHelpLayout` hook for handling dialog layout and state management.
- Added context for dialog help management with `DialogHelpContext` and `DialogHelpLabelsContext`.
- Developed utility functions for determining dialog help presentation modes.
- Included tests for dialog help functionality and layout behavior.
- Created a Python script for auditing contextual help in real forms.
2026-10-25 17:47:54 +01:00

21 KiB
Raw Blame History

Patrón de diseño de cuadros de diálogo

Fuente normativa para crear o modificar diálogos de Metis Desk. Recoge las decisiones aplicadas a los editores de plantillas de activos y recursos y a los selectores PickerList. La guía de componentes describe sus APIs y consumidores; el inventario de modales recoge las implementaciones existentes.

Composición y reutilización

  • Cada interacción modal con responsabilidad propia vive en un componente y fichero .tsx independientes. El padre decide su apertura y aporta datos y callbacks.
  • Reutilizar FormDialog, FormBody y FormActions de @repo/rdx-ui, apoyados en las primitivas de shadcn/ui y Base UI. Consultar los patrones existentes antes de crear otro.
  • Mantener los bloques visuales genéricos independientes de entidades, DTO, consultas y endpoints. El módulo aporta textos traducidos, controles, datos y acciones.
  • Aplicar el mismo patrón a alta, edición y copia y a entidades equivalentes. Conservar los campos y restricciones propios de cada entidad, como el uso de las plantillas de recursos.

Estados vacíos con incorporación

Cuando un paso todavía no contiene elementos y permite incorporarlos, reutilizar EmptyState de @repo/rdx-ui/components/empty-state: icono, título, descripción y acciones aportados por el consumidor. Mantener los textos traducidos y las acciones independientes, con sus permisos y estados deshabilitados. El componente llena el ancho y puede crecer en un contenedor flex para ocupar el alto restante. Para distribuir ese espacio desde el cuerpo del diálogo, utilizar FormBody contentLayout="fill" y mantener la cadena de crecimiento hasta el estado vacío. No depender de min-height: 100% en contenedores intermedios con altura automática. Conservar el scroll, la altura exterior estable. La ayuda complementaria se consulta bajo demanda desde la cabecera.

Grupos, títulos y separación

  • Las etiquetas de campos utilizan FormLabel de @repo/rdx-ui. Añadir required solo cuando el campo sea obligatorio: muestra un asterisco al final en color destructive. Los campos optativos muestran únicamente su nombre, sin «(opcional)».

  • La marca sigue las mismas condiciones que la validación del campo, también en selectores, características de plantillas y fechas condicionales. Conservar htmlFor, los atributos de validación y la ayuda accesible del control. No incluir el asterisco en las traducciones ni en el nombre de la característica. Los grupos con leyenda reutilizan RequiredIndicator.

  • Usar FormSectionTitle para los títulos de grupo: texto pequeño, seminegrita, mayúsculas, espaciado entre letras y color atenuado (text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-4). Conservar el nivel semántico del encabezado; las mayúsculas pertenecen a la presentación CSS y los textos pasan por i18n.

  • Cuando el título comparte fila con contadores o acciones, usar FormSectionTitle layout="inline" y alinear la fila con items-center. El título no añade margen inferior en este modo: la separación respecto al contenido pertenece al contenedor. Mantener título y contador en un mismo grupo flex para que no se separen al ajustar el ancho. Conservar el nivel semántico del encabezado y reservar el margen habitual para títulos independientes.

  • El título principal del diálogo y los nombres editables de secciones tienen su propia función. El nombre editable de una sección va en su cabecera, junto a sus acciones.

  • Para los datos generales, preferir FormSection variant="plain", sin borde ni relleno de tarjeta. Los grupos repetidos que necesitan identidad visual usan borde y fondo semánticos discretos. Evitar acumular marcos y rellenos en grupos anidados.

  • Mantener más separación entre grupos principales que entre controles relacionados. En el patrón de plantillas, TemplateMetadataSection deja 36 px antes del grupo de secciones (20 px del cuerpo y 16 px adicionales); las columnas y tarjetas se separan 16 px.

  • En editores de campos agrupados por secciones, mostrar en cada fila su posición dentro del grupo como 1/3, 2/3, etc. Reiniciar la numeración en cada sección y en «Sin sección», y derivarla del orden actual, también después de mover, añadir o quitar campos. Mantener esta información en el nombre accesible del asa.

  • Situar las acciones locales en la cabecera del grupo al que afectan. Por ejemplo, «Añadir campo» se ofrece en «Campos» y lleva implícita la sección de destino.

Distribución responsive y alturas

  • Decidir las columnas por el ancho disponible del contenedor, mediante container queries. En espacios estrechos, apilar el contenido y permitir que nombres y acciones se adapten.
  • En formularios con nombre, icono y descripción, reutilizar TemplateMetadataSection: nombre e icono debajo a la izquierda, descripción a la derecha, en dos columnas iguales desde 560 px. Ambas columnas tienen igual altura y el área de descripción ocupa el alto restante. En una columna, el recorrido es nombre, icono y descripción.
  • Para secciones repetidas equivalentes, usar una columna con poco espacio y dos cuando sus controles quepan con comodidad. TemplateSectionsEditor sectionLayout="responsive" cambia a dos columnas desde 1.000 px. Las secciones de cada fila tienen la misma altura, determinada por la más alta, y se reajustan al plegar, desplegar o cambiar su contenido.
  • Mantener el orden de izquierda a derecha y de arriba abajo en el DOM, la numeración, el teclado y el arrastre. No introducir un orden visual diferente mediante CSS.
  • Cada grupo de campos responde a su propio ancho, también cuando su sección ocupa media fila. El bloque «Sin sección» permanece debajo de las secciones, a ancho completo.
  • No convertir formularios sencillos en cuadrículas de secciones ni imponer alturas absolutas que superen la ventana. La estabilidad de la altura exterior se rige por la regla siguiente; las secciones interiores conservan altura según su contenido. Los umbrales anteriores corresponden a los componentes citados; otros contenidos deben elegir su disposición según el espacio que necesitan sus controles.

Estabilidad de altura del modal

Esta regla se aplica a los cuadros de diálogo actuales y futuros. Al crear o modificar un diálogo, comprobar si su contenido puede variar mientras permanece abierto: pasos, selecciones, altas anidadas, filtros, carga, errores, ayuda plegable o grupos condicionales. En esos casos, la altura exterior debe mantenerse estable durante toda la interacción. No debe crecer, encogerse ni desplazar el pie por cada elección del usuario.

  • Utilizar FormDialog layout="stable" para formularios y asistentes con contenido variable, como el alta de puestos. Reservar la altura desde la apertura, también cuando el primer paso sea corto o los datos todavía estén cargando.
  • La altura se adapta a la ventana: en móvil ocupa la altura dinámica disponible menos 2 rem de margen; desde sm, el menor valor entre el 85 % de la altura dinámica y 900 px, respetando siempre el margen. Puede reajustarse al cambiar la ventana, orientación o espacio disponible del teclado; no depende del contenido ni se mide con JavaScript.
  • Conservar cabecera y pie visibles, min-h-0 en la cadena flex, FormBody desplazable y espacio reservado para la barra vertical. El contenido puede cambiar de alto dentro del cuerpo, sin recortar campos, errores o acciones ni reducirlos para hacerlos caber.
  • Mantener layout="picker" para selectores con tabla que ocupa el espacio restante; ya proporciona altura estable y su distribución específica. No usarlo para fijar la altura de formularios que no sean selectores.
  • layout="content" queda para confirmaciones y diálogos breves cuyo contenido no cambia de tamaño. Si incorporan contenido variable, aplicar la variante estable al modificarlos.
  • Revisar pasos cortos y largos, listas vacías y extensas, carga/error y altas anidadas, tanto en móvil como en escritorio. Deben conservarse la altura exterior y la posición de las acciones, con acceso mediante scroll y teclado a todo el contenido.

Secciones plegables y filas compactas

  • Las secciones extensas o repetidas utilizan la primitiva Collapsible mediante TemplateSectionEditor. Empiezan abiertas y conservan el estado de apertura por identidad al reordenarse. El nombre, el agarre y las acciones permanecen en la cabecera.
  • Conservar los controles al plegar. Si la validación detecta un control inválido oculto, abrir la sección antes de enfocarlo. Plegar no modifica los datos.
  • En listas de configuración de campos, utilizar la presentación compacta TemplateSectionsEditor fieldLayout="compact": agarre, switch de obligatoriedad, nombre, flechas de desplazamiento y papelera. Evitar una segunda fila de controles cuando esta configuración sea suficiente. Esta pauta corresponde a referencias de campos dentro de una plantilla; los formularios de edición de sus valores conservan sus controles propios.
  • Dar nombre accesible a los switches y botones de icono, incluyendo el campo o la sección. La obligatoriedad conserva su etiqueta accesible y ayuda al pasar el ratón.
  • Reutilizar IconPicker como desplegable compacto con icono y texto, incluida la opción sin icono. Evitar una fila permanente con todas las opciones de iconos.

Ordenación y selección de campos

  • Reutilizar useDragReorder, ReorderHandle y el indicador de destino. Mantener flechas explícitas y ordenación por teclado; el arrastre no es la única forma de cambiar el orden.
  • Las secciones se ordenan entre sí y los campos dentro de su sección. Conservar el foco y bloquear los movimientos imposibles o las acciones durante el guardado. Cancelar un arrastre no cambia los datos y guardar confirma el borrador del formulario.
  • Cuando se incorporen varias referencias de un catálogo, utilizar un selector múltiple en diálogo en lugar de un desplegable que obligue a añadirlas una a una. En plantillas, reutilizar CatalogFieldPickerDialog desde la cabecera de cada grupo de campos, sin un bloque independiente de incorporación tras los datos generales.
  • Confirmar la selección como lote, conservarla al buscar o paginar y descartar la selección provisional al cancelar. Respetar límites, disponibilidad y ausencia de duplicados.
  • El alta rápida opcional se ofrece a la izquierda del pie mediante leadingActions. Debe poder ocultarse o deshabilitarse desde el consumidor. Mantener el selector y su estado durante el alta anidada; crear el elemento no confirma por sí solo el lote seleccionado.

Diálogos de selección PickerList

Implementación y evidencias de capacidad: L02.1. En selectores locales, mantener min-h-0 en wrappers flex y no imponer shrink-0 a toda la cadena de resultados. EntityTable layout="fill" reserva su presupuesto operativo con herramientas y paginación compactas; no sumar mínimos independientes en RadioGroup/Tabs. Verificar clientHeight y clipping de filas, no solo el alto del modal. El objetivo en 320×640 es mostrar dos opciones ordinarias completas; si el contenido extenso lo impide, documentar el presupuesto y demostrar acceso íntegro mediante scroll. La ayuda breve utiliza PickerHelp bajo demanda desde la cabecera: conserva instrucciones y destino en un Popover accesible sin reducir las filas al desplegarlo. No ocultar información obligatoria ni crear focus traps o listeners Escape manuales. En ventanas muy cortas, el scroll de contingencia del cuerpo conserva acceso a controles; no encadenar su gesto con el desplazamiento de resultados.

  • Todos utilizan FormDialog layout="picker" y EntityTable layout="fill": altura estable adaptada a la ventana, independiente de búsquedas, carga y número de resultados. La tabla ocupa el espacio restante y mantiene desplazamiento interno con espacio reservado para la barra; búsqueda, filtros, paginación y acciones permanecen estables.
  • Mantener el área de resultados incluso vacía y las cabeceras visibles durante el scroll. En ventanas pequeñas, permitir el desplazamiento adicional del cuerpo para acceder a todos los controles.
  • La columna selection es la primera, antes del nombre: anchura ajustada al checkbox o radio, poco espacio hasta el nombre y cabecera visualmente oculta con nombre accesible. No se ordena, no se oculta y no se ancla a la derecha como las acciones.
  • Proteger también la identidad principal con hideable:false. Las preferencias incompatibles no pueden dejar radios/casillas sin un nombre visible. Los rangos utilizan el sustantivo de la entidad, con singular/plural es/en, nunca el título «Seleccionar…».
  • Pulsar una zona no interactiva de una fila disponible alterna su checkbox o selecciona su radio. Reutilizar onRowClick de la tabla; los controles internos ejecutan solo su propia acción, sin duplicar cambios. Respetar filas deshabilitadas y navegación por teclado.

Foco inicial en la búsqueda rápida

Regla para todos los diálogos PickerList actuales y futuros: al abrirlos, el foco inicial debe situarse en el cuadro de búsqueda rápida, listo para escribir. No debe quedar en las pestañas, filtros, controles de selección o botones del pie.

  • Reutilizar FormDialog layout="picker" y la búsqueda de EntityTable, que se identifica con data-quick-search. FormDialog establece el foco mediante initialFocus de Base UI, limitado al input del propio modal. Los selectores con búsqueda propia deben mantener el mismo atributo en su input y la búsqueda debe estar montada y habilitada al abrir.
  • Mantener la búsqueda visible y disponible durante carga, errores y resultados vacíos. No esperar a los datos para montarla ni mostrar un mensaje de vacío mientras se carga.
  • Aplicar el foco al abrir, también al reabrir; no volver a enfocarlo por cada tecla, filtrado, cambio de pestaña, paginación o actualización de datos. No utilizar temporizadores ni efectos que roben el foco durante la interacción.
  • Mantener el aislamiento de los diálogos anidados y la restauración de foco de Base UI: cerrar un alta relacionada devuelve el foco a su disparador dentro del picker; cerrar el picker devuelve el foco al control que lo abrió.
  • Verificar apertura con ratón y teclado, reapertura, carga inicial y altas anidadas. El foco visible y el uso de Tab, flechas y Escape deben seguir funcionando.

Cuerpo, pie y accesibilidad

  • Elegir la anchura del diálogo según su contenido y respetar los márgenes de la ventana. Mantener cabecera y pie visibles y el cuerpo desplazable cuando el contenido lo requiera.
  • Las altas guiadas de recursos y activos desde el inventario de compañía siguen tres pasos: plantilla, características y datos finales. Reutilizar FormProgress y FormStepActions, validar antes de avanzar y conservar el borrador al retroceder. Guardar se muestra al final; ayuda, cancelación y errores permanecen dentro del diálogo. Los puestos y perfiles siguen cuatro pasos adaptados a sus propias referencias; comparten FormProgress, FormStepActions y FormHelp. La cuenta de un perfil sigue siendo opcional y su ordenador se deriva del puesto, sin añadir una segunda elección del dispositivo. El alta usa tamaño medio y la edición completa conserva tamaño grande.
  • Los formularios con contenido variable mantienen altura exterior estable según la regla de estabilidad; sus grupos interiores conservan altura según contenido. No introducir pestañas ni pasos para resolver solo espacio.
  • Declarar la ayuda contextual complementaria mediante ContextHelp, FormHelp, PickerHelp o DialogHelp dentro de FormDialog. El sistema compartido la sitúa bajo demanda desde el botón ? de cabecera, inmediatamente antes de X. No añadir bloques permanentes al pie salvo una excepción funcional justificada. La API y política de ayuda definen contenido, modos y accesibilidad. Mantener errores, advertencias críticas e instrucciones imprescindibles visibles junto al contexto que los origina.
  • Conservar foco, cancelación, cierre por teclado, estados de carga, error, vacío y envío. No cerrar ni limpiar el formulario antes de confirmar una operación correcta.
  • Validar los cambios de interacción con pruebas dirigidas y revisar visualmente los anchos estrecho y amplio, nombres largos, secciones de distinta altura, plegado y resultados vacíos cuando correspondan. Informar de las comprobaciones que no se hayan podido ejecutar.

Ayuda contextual bajo demanda

El patrón reduce la información secundaria persistente sin eliminar contenido. Se aplica a formularios, pickers, wizards y consultas con ayuda complementaria. La ayuda que constituye la tarea principal de un diálogo se mantiene visible (presentation="inline" en ContextHelp).

FormDialog conserva el contrato de apertura, cierre, foco, tamaño y layout. Proporciona los destinos de cabecera y ayuda; DialogHelp declara title: string, children: ReactNode y presentation?: "auto" | "compact" | "expanded" (auto por defecto). ContextHelp mantiene sus props de descripción, ejemplos, ámbito, orientación e icono; FormHelp reutiliza ese contrato. Dentro de FormDialog adoptan el nuevo slot automáticamente. Fuera de él siguen mostrando contenido visible. No es necesario elevar traducciones desde los formularios.

<FormDialog {...dialogProps}>
  <AssetForm />
  <DialogHelp title={t("helpTitle")} presentation="expanded">
    <ExistingTranslatedInstructions />
  </DialogHelp>
</FormDialog>

Declarar un único bloque de ayuda general por diálogo. Si AssetForm ya declara ContextHelp o FormHelp, no añadir otro DialogHelp al padre. Los diálogos anidados tienen su propio contexto.

  • compact: explicación breve de una tarea. Popover desde 30rem de ancho efectivo del diálogo; vista interna cuando falta espacio. PickerHelp declara esta política.
  • auto: contenido estructurado, como descripción + ámbito + ejemplos + orientación. Equivale a expanded; nunca mide caracteres ni texto del DOM.
  • expanded: panel integrado de 20rem a la derecha desde 60rem de superficie, reservando al menos 40rem al formulario; por debajo, vista de ayuda dentro del mismo modal.
  • Se miden el diálogo real y la fuente raíz mediante ResizeObserver. Un modal estrecho en un monitor amplio utiliza vista interna. El panel no ensancha la superficie exterior. Al aparecer, el panel utiliza una animación CSS de entrada de 200 ms con fundido y desplazamiento corto desde la derecha. Solo anima opacity/transform y se desactiva con prefers-reduced-motion. La animación se aplica al contenedor compartido, también al reabrirlo.
  • En vista interna el formulario permanece montado y conserva geometría, scroll y estado, con visibility:hidden, inert y aria-hidden. Sus acciones no se pueden activar. La ayuda ofrece «Volver al formulario» y mantiene disponible X. En panel, el formulario y su pie siguen siendo utilizables; la ayuda dispone de desplazamiento y cierre independientes. El contenido reserva al menos tres líneas de altura; con texto muy ampliado, la región completa de ayuda también permite scroll para conservar legibilidad y acceso al retorno.
  • El botón de ayuda usa icono CircleHelp del catálogo Tabler, Button icon-sm/ghost, nombre accesible traducido, aria-expanded y aria-controls cuando está abierto. El título dispone de espacio flexible; los controles no se solapan ni reservan un hueco de ayuda ausente.
  • Al abrir panel/vista o cambiar de modo se enfoca su encabezado. Al cerrar se devuelve foco a ?. Escape cierra primero la ayuda mediante el contrato de cierre de Base UI; otra pulsación cierra el diálogo. Los controles anidados conservan las reglas de sus primitivas.
  • Popover utiliza las primitivas instaladas, su posicionamiento adaptativo, portal y gestión de foco; no se crea otro modal, Sheet superpuesto ni focus trap propio.
  • Las etiquetas genéricas se suministran con DialogHelpLabelsContext. La aplicación las resuelve en core.dialogHelp (es/en); rdx-ui no conoce namespaces ni entidades.
  • Conservar sin reformular textos, enlaces, listas, ejemplos y traducciones. Los campos, validaciones, consecuencias de operaciones, seguridad y estados vacíos siguen visibles.

No duplicar triggers, cierres, detectores responsive ni paneles en módulos. No clasificar por longitud del DOM, usar hover como único acceso, desmontar el formulario ni introducir otra capa modal para la ayuda. Comprobar teclado, foco, responsive, scroll y estados en navegador; no atribuir a pruebas automatizadas validación con lectores de pantalla reales.

Véase auditoría e inventario final para migraciones, excepciones, evidencias y límites de las comprobaciones.