metis-desk/docs/patron-de-dialogos.md
David db57346c24 feat: add PickerHelp component and browser fixture for picker UI
- Introduced PickerHelp component for contextual help in pickers.
- Created PickerBrowserFixture for testing picker components in a browser-like environment.
- Implemented a comprehensive regression test suite for picker UI using Playwright.
- Added support for localization in picker components and tests.
2026-10-22 09:35:02 +02:00

229 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](componentes-de-interfaz.md) describe sus APIs y consumidores;
el [inventario de modales](inventario-de-modales.md) 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 y la ayuda inmediatamente encima del pie del modal.
## 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](selectores-accesibilidad-l02-1.md).
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 extensa puede utilizar `PickerHelp` junto al pie: 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.
- Reutilizar `FormHelp` para la ayuda contextual general inmediatamente antes de `FormActions`. En los
diálogos de altura estable debe quedar anclada abajo, fuera del scroll principal:
campos, progreso y errores se desplazan en `FormBody`, mientras la ayuda permanece
pegada al pie incluso en pasos cortos. Eliminar márgenes superiores innecesarios y
conservar el padding normal junto al footer. Limitar su altura y permitir scroll propio al
desplegar ejemplos, para no ocultar los campos ni las acciones en ventanas pequeñas.
Mantener errores junto al contexto que los origina y las acciones dentro del formulario.
- 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.