65 KiB
Componentes de interfaz
Metis Desk usa shadcn/ui, con Base UI y el estilo base-vega, siguiendo la organización de factuges-erp.
packages/shadcn-ui contiene únicamente el código del registro oficial y no se adapta ni se
extiende desde este proyecto. Se consume mediante @repo/shadcn-ui.
Los componentes genéricos compuestos o reutilizables propios viven en packages/rdx-ui y se
consumen mediante @repo/rdx-ui. Los componentes que conocen contratos, textos, consultas o
reglas de Metis viven en el módulo o aplicación que los utiliza; nunca en ninguno de los dos
paquetes genéricos.
El patrón de diseño de cuadros de diálogo es la fuente normativa para componer grupos, columnas responsive, filas compactas y selectores. Esta guía describe los componentes que lo implementan y sus contratos.
Componentes actuales
- Button: volver a comprobar la conexión.
- Card: bloques de información y estado.
- Select: idioma y selección de sistemas, entornos, cuentas, personas y demás relaciones, con navegación por teclado.
- Input y Label: credenciales y datos de usuario.
- Checkbox: filtros de listado, selección múltiple y configuración de columnas.
- SwitchField: campos binarios de formularios, como estado activo, VPN, predeterminado o requerido.
- Table: listados de usuarios, compañías, personas y departamentos.
- Textarea: dirección opcional de las sedes, con varias líneas.
- Dialog: primitiva de shadcn/ui, con foco y cierre por teclado.
- RDX UI: FormDialog, FormActions, ContextHelp, SelectWithAction, SwitchField, EntityTable y StatisticCard y PhoneLink.
Convención de navegación
Los destinos de secciones y fichas existentes siguen Rutas del cliente. Usar enlaces reales para esos destinos y botones para acciones o diálogos locales.
Los listados presentan datos, búsqueda y acciones. El botón de alta se coloca en la cabecera, a la derecha. Los formularios se abren en modal como norma general, con anchura adaptable y desplazamiento si el contenido lo requiere. No mostrar formularios junto al listado. Las ediciones se abren mediante una acción explícita. Compañías, personas, departamentos, asignaciones, sedes y usuarios siguen este criterio. Las personas, los departamentos y las asignaciones se crean y editan en modales. El historial se consulta en un diálogo. Catálogos, cuentas, grupos, recursos, permisos y necesidades siguen la misma convención. Los permisos de una necesidad se eligen mediante casillas shadcn/ui.
TanStack Table filtra el listado de compañías por nombre; React Hook Form gestiona los formularios con los contratos Zod compartidos.
El paquete exporta componentes individuales. Importar solo el componente necesario:
import { Button } from "@repo/shadcn-ui/components/button";
El tema base se define en packages/shadcn-ui/src/styles/globals.css; las extensiones propias
se declaran fuera del vendor. La aplicación importa ese CSS y declara las fuentes de Tailwind de
los módulos y de packages/rdx-ui.
Usar tokens semánticos como bg-background, text-foreground y border-border en vez de repetir colores por pantalla.
Tipografía
La aplicación carga localmente Inter y JetBrains Mono desde apps/web/src/fonts, con
font-display: swap. Georgia usa la fuente disponible en el sistema, seguida de
Times New Roman. Tailwind 4 expone las tres familias mediante font-sans,
font-serif y font-mono: Inter es la tipografía general; Georgia se reserva para
contenido editorial puntual; JetBrains Mono, para código, identificadores y valores técnicos.
Las licencias OFL 1.1 de las dos fuentes descargadas acompañan sus respectivos archivos.
Añadir componentes
SectionNavigation, en modules/companies/src/web/shared/ui, compone Card de la
biblioteca y BrowserLink de core para índices de secciones. Recibe una etiqueta
de navegación y elementos con href, título, descripción y valor numérico
opcionales. No conoce DTO, consultas ni endpoints; sus consumidores aportan los
textos traducidos y únicamente valores conocidos. Se reutiliza en el resumen de
compañía y en los índices de catálogos globales y de compañía, con foco visible y
enlaces que pueden abrirse en otra pestaña. No se modifica el paquete vendor.
Para componentes propios de un módulo, consultar primero la estructura web por entidades y operaciones y los componentes equivalentes existentes. Ubicar cada componente en la entidad y operación responsables, o en su ámbito compartido cuando tenga varios consumidores reales. Los componentes específicos de Metis Desk no pertenecen al paquete visual genérico.
La incorporación o actualización de primitivas en el paquete vendor requiere una instrucción
explícita del usuario según AGENTS.md. En ese caso, utilizar el procedimiento siguiente.
Desde la raíz del proyecto, dentro de WSL:
pnpm --filter @repo/shadcn-ui ui:add input
El CLI está fijado en package.json. Revisar los archivos y dependencias añadidos antes de validar. No usar --overwrite sin revisar las adaptaciones locales. apps/web/components.json también permite al CLI localizar el paquete compartido desde la aplicación.
Después de generar código, separar los imports de tipos con import type y ejecutar las comprobaciones del proyecto. No añadir todo el catálogo ni crear envoltorios sin una necesidad concreta.
Dependencias
Base UI proporciona interacción accesible y gestión del foco. class-variance-authority define variantes del botón; cn combina clases de Tailwind. lucide-react aporta los iconos del selector y tw-animate-css sus transiciones. shadcn es una herramienta de desarrollo; su hoja de estilos común se incorpora al compilar.
Guías oficiales: monorepos e instalación manual.
Ayuda contextual
ContextHelp muestra una explicación breve y permite desplegar ejemplos y recomendaciones mediante teclado. EntityHelp aporta contenido específico en español e inglés para compañías, personas, departamentos, sedes, asignaciones y todas las secciones de cuentas y accesos. Los catálogos explican si la definición es global o exclusiva de la compañía. Los ejemplos son orientativos: nunca se convierten en valores del formulario.
La ayuda se presenta en una tarjeta con icono circular, título y explicación; debajo, una banda desplegable contiene los ejemplos y recomendaciones. ContextHelp admite icon?: ReactNode para sustituir el icono informativo predeterminado, por ejemplo icon={<UsersIcon />}. El icono es decorativo y hereda el tamaño y color del contenedor. Se conservan los tokens del tema, los textos aportados por cada módulo y el despliegue nativo por teclado.
El botón Ayuda de Cuentas y accesos y Catálogos informáticos permite consultar la explicación de la sección sin abrir un alta. La misma ayuda aparece dentro de los modales de alta y edición. FormDialog utiliza una variante amplia para personas y formularios con más campos; el número de campos no cambia la navegación a una página propia.
La ayuda de los formularios se coloca en su parte inferior, inmediatamente antes de FormActions. Este pie compartido reúne Guardar y Cancelar dentro del formulario, conservando el envío y el cierre accesible del diálogo. Los diálogos de consulta mantienen únicamente Cerrar.
SelectWithAction: desplegable con acción integrada
El componente compartido vive en @repo/rdx-ui/components/select-with-action. Compone Select,
Button y ButtonGroup de shadcn/ui (Base UI, Vega). Presenta selección y botón «+» como un único
grupo visual, manteniendo controles separados para teclado y lectores de pantalla. ButtonGroup
utiliza fieldset para expresar semánticamente la agrupación.
Su contrato recibe id, etiqueta, opciones, placeholder, valor controlado y callback de selección. La acción es opcional y contiene etiqueta accesible, callback y estado deshabilitado. Admite opciones deshabilitadas, estado inválido, descripción accesible y obligatoriedad. Deshabilitar el selector también deshabilita la acción. El botón usa type=button y no envía el formulario.
No conoce compañías, HTTP, formularios ni modales: esas responsabilidades pertenecen al consumidor. No contiene textos propios que deban traducirse.
SwitchField: activación y desactivación
SwitchField vive en @repo/rdx-ui/components/switch-field. Compone el Switch de shadcn/ui
con su etiqueta accesible y recibe un valor controlado, onCheckedChange, id, texto y estado
deshabilitado. No conoce React Hook Form ni ningún DTO: el formulario aporta el valor y actualiza
su fuente de verdad.
Se usa para cambios binarios que expresan una propiedad o un estado de la entidad, incluidos activo, requerido, contacto, VPN, predeterminado y relación simétrica. No sustituye Checkbox en filtros ni en elecciones de varias opciones, donde la casilla comunica correctamente inclusión, exclusión o selección múltiple.
Formularios que permiten alta rápida
Sistema permite crear un sistema propio de la compañía mediante un modal adicional. Entorno permite crear un entorno para el sistema activo seleccionado; su acción no aparece antes de elegir un sistema. Los campos fijos en edición no ofrecen alta rápida. Uso y otros valores cerrados tampoco ofrecen altas.
QuickCreateDialog conserva los valores del formulario principal, actualiza el catálogo y selecciona el nuevo UUID después de guardar. Usa las rutas, protección CSRF y reglas de negocio existentes. Un nombre activo ya existente permite usar ese registro sin duplicarlo. Cancelar el modal adicional conserva la cuenta sin cambios. El elemento creado permanece en el catálogo aunque se cancele después la cuenta.
La ayuda aparece antes del pie con Guardar y seleccionar / Cancelar. Solo hay un nivel de alta adicional; el nuevo formulario no vuelve a ofrecer altas rápidas.
La revisión de los formularios extiende este patrón a personas, departamentos, cuentas, grupos, recursos, tipos de recurso, permisos y plantillas. Las asignaciones de departamento permiten añadir el primero sin abandonar el formulario. Se reutilizan los formularios y rutas existentes, manteniendo su validación y ayuda inferior.
Los entornos necesitan un sistema activo seleccionado. Los permisos nuevos heredan el tipo del recurso elegido. Al crear un grupo desde una pertenencia, sistema y entorno quedan fijados a los de la cuenta. Las plantillas siguen siendo globales; el modal lo explica y carga únicamente sus opciones globales. Los elementos creados desde un selector están activos para poder seleccionarlos.
Uso de cuenta, estado de configuración e idioma son listas cerradas. Las referencias históricas de cobertura (necesidad, autorización, titularidad y pertenencia) conservan sus operaciones propias. Los campos inmutables en edición no ofrecen altas. Los permisos múltiples de una necesidad continúan como casillas.
El catálogo de activos utiliza un diálogo de selección múltiple basado en EntityTable para añadir campos a una plantilla y conserva el alta rápida mediante un modal independiente. Los tipos de dato constituyen una lista cerrada. Las plantillas de serie admiten edición modal desde el catálogo global; para personalizar exclusivamente una compañía se crean copias. Los campos de serie admiten edición modal desde el catálogo global.
El inventario añade altas rápidas de plantilla y sede en los activos. La plantilla creada desde un activo solo ofrece campos existentes para mantener un único nivel adicional de modal. Las características utilizan controles según su tipo; los valores sí/no conservan un estado vacío distinto de No.
Listados de entidades
El estándar se define en Patrón de listados de entidades, con inventario de pantallas y excepciones. EntityListHeader, EntityTable, EntityRowActions, EntityAction y EntityStatus viven en @repo/rdx-ui/components/entity-list.
EntityTable recibe datos y columnas tipadas; utiliza TanStack Table para búsqueda, ordenación, visibilidad y paginación local. Admite búsqueda y filtros controlados por el módulo, conservando su semántica y ámbito. Las columnas de identificación y acciones quedan visibles. Los menús utilizan Base UI y conservan los callbacks de las acciones originales. No realiza peticiones ni conoce DTO de negocio.
La primera columna con value, excluida actions, se ordena inicialmente en ascendente. Una columna puede declarar defaultSort: "asc" | "desc" para elegir otro criterio; declarar como máximo una por tabla. La ordenación se aplica antes de paginar, conserva empates y se refleja en la cabecera y el selector, incluso sin filas. «Orden inicial» restaura este criterio y las cabeceras alternan dirección sin eliminar la ordenación. Las tablas sin columnas comparables conservan el orden recibido.
La paginación es local sobre los conjuntos que las API actuales ya devuelven. No simula paginación de servidor. La selección del activo para el panel sigue perteneciendo a Inventario, fuera de la tabla compartida.
Los inventarios de activos y recursos componen las primitivas Tabs de shadcn/ui con EntityTable para filtrar por plantilla. La barra admite desplazamiento horizontal y navegación por teclado; incluye «Todo» y las plantillas del catálogo de la compañía. Un único panel conserva la tabla, su ordenación y sus columnas al cambiar de pestaña. filterKey reinicia la página; filtersApplied y onClearFilters integran el filtro con los estados vacíos y la limpieza común del listado.
EntityTable muestra — en las celdas cuyo renderizador devuelve ausencia, texto vacío, espacios, arrays vacíos o fragmentos sin contenido. Los consumidores devuelven ausencia directamente, sin textos como «Sin indicar»; véase celdas sin valor.
EntityTable separa internamente la configuración de columnas/ordenación, el contenido semántico y la paginación; useEntityTable concentra el estado de representación. entityLabel permite contar «plantillas», «campos» o «recursos» aunque el título del espacio sea más amplio. Los tamaños disponibles son 10, 25, 50 y 100; una opción vacía nunca cambia el tamaño a cero.
Se declara @tanstack/react-table en el paquete compartido porque es quien consume ahora la librería; se reutiliza la versión 8.21.3 que ya utilizaba Compañías. No se introduce un segundo sistema de tablas.
Catálogo de marcas y selector funcional
BrandCatalogPage reutiliza EntityListHeader/EntityTable con búsqueda, filtro de estado, columnas, ordenación y paginación. BrandEditorDialog compone FormDialog y BrandForm tanto para la gestión global como para las altas relacionadas; el formulario conserva ContextHelp y FormActions.
BrandLogo y BrandLogoField muestran y permiten seleccionar SVG estáticos locales, con vista previa, errores de campo y acción de quitar. La columna de logotipo reutiliza esa presentación. Los límites, validación y fuentes se describen en Catálogo global de marcas.
DeleteBrandDialog compone FormDialog y DialogFooter para confirmar la eliminación de una marca personalizada. El listado solo coordina selección y cierre; el controller de brands/delete gestiona mutación y caché. Conserva cancelación y Escape, bloquea cierre y botones durante la petición y mantiene errores en el modal. Las referencias actuales o históricas se protegen en el servidor.
BrandSelect, dentro del módulo assets, adapta SelectWithAction al catálogo remoto: filtra marcas activas, conserva la selección inactiva actual y textos anteriores, ofrece alta rápida y respeta una sola anidación. El borrador padre se conserva y la nueva marca se selecciona por UUID. El componente genérico no conoce BrandDTO, consultas ni endpoints. Véase Catálogo global de marcas.
Componentes modales de los módulos
La ubicación de componentes, formularios y clientes HTTP sigue la estructura web por entidades y operaciones.
El inventario de modales localiza las interacciones de alta, edición, copia, confirmación, detalle, historial, selección, ayuda y configuración, incluyendo las altas rápidas dentro de otros formularios. La regla normativa de organización reside en AGENTS.md.
Las páginas coordinan apertura, selección y callbacks. El componente modal posee su estructura y la lógica que le pertenece; el formulario mantiene validación, borrador, errores y guardado. Se reutilizan FormDialog y FormActions sin modificar su implementación vendor. Los diálogos específicos de entidades viven junto a sus consumidores, no en la biblioteca visual genérica.
CompanyEditorDialog, PersonEditorDialog, DepartmentEditorDialog, SiteEditorDialog, BrandEditorDialog y el formulario reutilizable ResourceEditor se reutilizan donde ya existen operaciones equivalentes. Los callbacks de guardado actualizan la selección del consumidor; no se cierra un formulario por el mero inicio de una mutación. Los historiales de consulta se derivan de los datos remotos actuales, mientras que los borradores de edición permanecen locales.
Acciones de fila ancladas
EntityRowActions muestra un botón icon-sm con EllipsisIcon, etiqueta accesible y título traducidos, sin texto visible repetido. EntityTable reserva la identidad de columna actions para operaciones: siempre visible, sin búsqueda/ordenación y fijada a la derecha mediante columnPinning de TanStack Table. EntityTableContent renderiza primero las columnas de datos y después la columna anclada, con cabecera/celdas sticky right-0, alineación derecha, anchura compacta y fondo opaco para evitar solapamientos al desplazar la tabla. Todos los listados consumidores heredan este comportamiento.
Tarjetas de indicadores
StatisticCard (@repo/rdx-ui/components/statistic-card) representa un indicador con etiqueta, valor, icono y tinte discreto. Admite carga/error, evita sustituir datos desconocidos por cero y utiliza un botón con foco visible cuando hay navegación. No depende de DTO ni endpoints. Inicio la reutiliza para sus cinco KPI; la organización de bloques y sus estados permanece local al dashboard.
Shell global
App conserva el acceso y el selector de idioma. CompaniesWorkspace compone sidebar,
header global y contenido con los destinos y estado de navegación existentes. El
header solo aparece en sesiones autenticadas. Los catálogos se seleccionan en la
sidebar; la navegación interna de cada compañía permanece en CompanyDetail.
Los tokens Tailwind 4 --color-shell-* de apps/web/src/global.css aíslan la paleta del shell de los
colores semánticos del resto del producto: superficie negra exterior, contenido
#F7F9FB, sidebar #0E282C, acento turquesa, texto y borde azul, radios 8/10/20px,
sidebar 228px, header 88px, búsqueda 41px, fila de navegación 48px y avatar 35px.
En móvil se conserva la navegación horizontal y envuelta, sin introducir drawer.
El buscador usa Input de shadcn/ui y filtra compañías cargadas y destinos globales; Escape limpia y los resultados se recorren mediante Tab. No existe un servicio de búsqueda transversal de entidades. La campana no muestra avisos ficticios: no existe servicio de notificaciones. El perfil usa disclosure nativo con foco visible y conserva las acciones de identidad y selección de idioma. No hay primitives Avatar ni DropdownMenu instalados; el avatar muestra iniciales del usuario real, sin inventar foto ni cargo. Configuración abre un FormDialog con las acciones existentes.
La sidebar global permite contraerse en escritorio de 228px a 72px mediante «Contraer menú» / «Expandir menú». Conserva iconos, nombres accesibles y títulos de ayuda también para compañías y subcatálogos. La navegación se desplaza por separado, manteniendo Configuración y el control de anchura en el pie visible. En móvil se conserva la navegación horizontal existente.
El plegado se conserva entre recargas y sesiones mediante la clave
metis-desk:sidebar-collapsed de localStorage, compartida por las sesiones de
ese navegador y origen, no por cuenta ni mediante el servidor. Solo true inicia
la barra plegada; valores ausentes o inválidos mantienen el estado desplegado.
Se guarda desde la acción de plegar/desplegar. Si el navegador bloquea la lectura
o la escritura, la barra sigue funcionando en memoria. No cambia el layout móvil.
El menú expandido y su pie comparten márgenes horizontales de 20px mediante
--spacing-shell-sidebar-inset y la utilidad px-shell-sidebar-inset. El header global usa
bg-shell-header (#FFFFFF), con un borde inferior border-shell-border, sobre el contenido #F7F9FB.
Estilos con Tailwind 4
La aplicación y la biblioteca base utilizan Tailwind 4.2.2 y el plugin oficial de Vite.
El CSS de la aplicación contiene únicamente imports, fuentes de detección y tokens @theme;
los layouts, estados y reglas responsive se expresan mediante utilidades en sus componentes.
No hay clases globales de componente desk-*, company-space-*, dashboard-* ni de inventario.
La implementación vendor de @repo/shadcn-ui no se modifica para personalizar el shell.
Los colores propios utilizan --color-shell-*; las dimensiones, --spacing-shell-*;
los radios, --radius-shell-* y --radius-panel; la sombra del desplegable,
--shadow-shell-popover. El resto de la interfaz conserva los tokens semánticos de shadcn/ui.
Los valores puntuales que no se repiten se declaran como utilidades arbitrarias, no como
tokens o abstracciones anticipadas. Los degradados conservan su interpolación CSS original.
Los breakpoints de la adaptación se declaran en píxeles para conservar los cortes anteriores:
shell (768px), inventory (1100px) y dashboard (1200px). Se usan consistentemente dentro
de cada layout para evitar conflictos de orden entre variantes con distintas unidades.
WorkspaceNavigationButton compone Button y comparte los niveles principal y de catálogo,
selección, foco y contracción. DashboardNavigationButton, DashboardStateBadge y
DashboardTable encapsulan patrones repetidos del dashboard; este último reutiliza Table.
Cada componente vive en su propio fichero y recibe acciones, textos o contenido del consumidor.
LanguageSwitcher permite ajustar su margen mediante className, sin selectores globales
dependientes de la cabecera que lo contiene.
Avatar de iniciales
InitialsAvatar (modules/companies/src/web/shared/ui/initials-avatar.tsx) es un
componente visual sin dependencias de compañías, DTO ni consultas. Recibe name
y atributos de span; admite className para tamaño y aria-hidden cuando el
control contenedor ya tiene nombre accesible. Se reutiliza en las compañías de
la sidebar y en el perfil del header.
Muestra la primera inicial de hasta dos palabras; conserva siglas de una palabra como IBM (hasta tres caracteres), y usa «?» para nombres vacíos. El fondo pastel se elige de una paleta de doce colores mediante un hash del nombre normalizado: mayúsculas, espacios y acentos no cambian el color. El color no depende del orden ni cambia al recargar; distintos nombres pueden compartir color. El texto oscuro mantiene contraste legible. Los avatares de compañías siguen visibles cuando se contrae el menú y conservan la selección y las ayudas del botón de navegación.
Avatar y logotipo de compañía
CompanyAvatar, CompanyIdentity y CompanyLogoField reúnen la presentación y selección del logotipo. Su contrato, límites y persistencia se documentan en Logotipos de compañías. Se usan tamaños de 24–28 px en navegación/listas y 40–56 px en detalle/formulario.
Avatar de persona
PersonAvatar reutiliza InitialsAvatar con los mismos colores y ajuste de texto que
CompanyAvatar. Muestra la foto versionada con carga diferida y recurre a las iniciales
si falta la foto o falla su carga. Conserva el nombre accesible, admite atributos de
Avatar y permite indicadores de estado como children. PersonAvatarGroup compone
los avatares del grupo y vive en un fichero independiente.
FieldSections y TemplateSectionsEditor
Componentes de @repo/rdx-ui independientes de los DTO, consultas y endpoints de Metis Desk. FieldSections recibe secciones, elementos con sectionId opcional y renderItem; presenta una cuadrícula plana cuando no hay secciones, o bloques desplegables accesibles con descripción y «Sin sección». Admite presentación de valores como listas de descripción y apertura ante errores externos.
TemplateSectionsEditor recibe secciones, referencias de campos, nombres, textos traducidos, generador de identidad y onChange. Permite ordenar secciones/campos, editar nombre/descripción, asignar campos y mantener su obligatoriedad mediante SwitchField. El módulo consumidor conserva el estado en React Hook Form y genera UUID v7. Los helpers removeTemplateSection y copyTemplateLayout conservan referencias y remapean identidades respectivamente. No se modifican los componentes vendor.
Formularios modales adaptativos
FormDialog admite size="small" | "medium" | "large". Los formularios sencillos mantienen el tamaño pequeño; personas y usuarios emplean el intermedio. wide sigue siendo un alias del tamaño grande: 90 % del ancho disponible, con máximo de 1.200 px y márgenes en móvil. No se ensanchan automáticamente las altas sencillas.
La cabecera permanece visible. Un formulario modal utiliza una raíz flex con altura mínima cero, FormBody para campos, errores y ayuda, y FormActions como hermano final fuera del cuerpo desplazable. Los botones permanecen dentro del formulario HTML original: se conservan envío por teclado, validación nativa, estados pendientes y cancelación. La ayuda queda al final del cuerpo, inmediatamente antes de las acciones. Los diálogos de consulta conservan el cuerpo desplazable y su pie externo.
FormSection agrupa datos relacionados mediante título accesible; la variante card añade un borde discreto y plain conserva el bloque sin marco. Ambas utilizan FormSectionTitle. Activos, recursos y plantillas lo utilizan para sus datos principales. Las características se distribuyen en dos bloques por fila cuando el contenedor dispone de al menos 900 px; los controles dentro de cada bloque mantienen una o dos columnas según el espacio. Una plantilla sin secciones puede aprovechar hasta tres columnas de características. Observaciones y ayuda ocupan toda la fila. La disposición responde al espacio del modal, no solo al ancho del navegador.
El desplazamiento vertical se conserva cuando el contenido supera la altura disponible; no se ocultan campos ni se reduce artificialmente su tamaño para eliminarlo. No se introducen pestañas ni páginas nuevas sin una necesidad específica del formulario.
Resumen de compañía
La pantalla CompanyOverview tiene composición independiente de DashboardPage (Inicio). Comparte tarjetas, bloques de avisos/actividad y consultas existentes, siempre fijando el ámbito al UUID de la compañía. El encabezado «De un vistazo» precede las acciones rápidas (activo y recurso), dos indicadores operativos y una franja navegable de personas, departamentos y sedes. No muestra compañías activas ni el resumen por compañías. La variante compacta de StatisticCard mejora la jerarquía del valor y la etiqueta sin modificar la presentación de Inicio.
La navegación de compañía empieza por Resumen; después agrupa Personas, Puestos de trabajo y Perfiles de usuario y finalmente las demás secciones. Los grupos tienen separación visual. Las operaciones de usuario permanecen en el menú superior y dejan de repetirse al pie del menú de compañía.
Fichas de consulta y resumen lateral
Las secciones de consulta evitan recuadros anidados y rellenos laterales: usan títulos compactos y un separador tenue entre grupos. Las agrupaciones de características siguen este mismo criterio en lectura; los formularios conservan su organización propia.
DetailSection ofrece agrupación plegable nativa (details/summary), abierta inicialmente, accesible por teclado. DetailField devuelve una fila semántica dt/dd para incluir en un dl: etiqueta y valor se alinean horizontalmente cuando el contenedor dispone de al menos 240 px y se apilan en espacios menores. El consumidor aporta los enlaces; el componente común les aplica cursor de enlace, color semántico y subrayado al pasar el puntero. Los valores ausentes permanecen como texto.
La ficha lateral de activos utiliza un ancho adaptable entre 320 y 400 px, acciones debajo del título y bloques de identificación, ubicación/asignación, características, observaciones, relaciones y asistencia. Conserva las secciones capturadas de la plantilla, incluyendo la compatibilidad con especificaciones antiguas sin agrupación; no modifica valores ni instantáneas. Las relaciones llevan a destinos existentes: activo relacionado a su ficha; sede, puesto y plantilla a sus listados/catálogos actuales. La vista completa e historial mantienen sus funciones. Las fichas de recursos reutilizan las filas de características. No se crean nuevas fichas laterales ni nuevas rutas.
Enlaces y controles de navegación
Los enlaces de contenido usan app-link: color semántico primary, cursor de mano, subrayado al pasar el puntero y foco visible por teclado. BrowserLink lo incorpora de forma predeterminada, conservando la navegación interna, los clics modificados, destinos externos y descargas. Los botones que presentan nombres navegables o «Ver todos» comparten este estilo sin alterar sus callbacks. Los controles deshabilitados no muestran cursor de enlace ni subrayado. La navegación lateral y los enlaces presentados como botones conservan los colores de su contexto para mantener contraste; las acciones de guardar, editar o cancelar mantienen su presentación de botón.
PhoneLink centraliza la visualización no editable de teléfonos: utiliza un enlace tel:, conserva extensiones para la llamada y presenta los números españoles de nueve dígitos en grupos de tres. Los números internacionales que no se pueden agrupar con seguridad conservan su estructura, sin inventar un formato nacional. Los formularios no lo usan: mantienen el valor editable original.
En el menú de secciones de una compañía, los elementos utilizan el color normal de texto y solo destacan su texto al pasar el puntero. Se mantienen el cursor de mano, el foco por teclado y el indicador de la sección actual. Esta excepción no afecta a enlaces dentro de listados, fichas o bloques del dashboard.
El cuerpo desplazable FormBody reserva 4 px interiores en ambos laterales y al inicio para el anillo exterior de foco (3 px). No debe eliminarse ese espacio: overflow-y-auto también recorta horizontalmente y produciría un foco asimétrico en campos pegados al borde. Se conserva el scroll y las acciones fijas.
Los listados comparten la preferencia entity-page-size en localStorage: el número de elementos por página elegido en un listado se restaura en todos los demás. Solo se aceptan los tamaños disponibles en el selector; si el almacenamiento no está disponible o contiene otro valor, se usa el tamaño inicial.
Confirmación de eliminación de registros
ConfirmDeleteDialog de @repo/rdx-ui presenta una confirmación modal con descripción de consecuencias, Cancelar y Eliminar. El consumidor aporta los textos traducidos y una operación asíncrona. Mientras se ejecuta, evita envíos repetidos y el cierre; si falla, mantiene el diálogo abierto y muestra un error. Solo se cierra tras completar la operación.
Los módulos conectan este componente mediante DeleteRecordDialog, separado de los menús de fila para que la confirmación permanezca montada cuando se cierre el menú. La operación invalida la consulta de la entidad y las revisiones de procesos de la compañía.
IconPicker
@repo/rdx-ui/components/icon-picker presenta un desplegable compacto mediante Select, SelectTrigger, SelectValue y SelectItem de shadcn/ui (Base UI). Muestra el icono seleccionado y su etiqueta traducida; cada opción combina icono y texto, incluida «Sin icono». Recibe opciones (valor, etiqueta e icono), valor actual, callback, estado deshabilitado y descripción opcional enlazada mediante aria-describedby. Su anchura máxima en escritorio es 16 rem y se adapta al espacio disponible en móvil. La etiqueta se asocia al control y los iconos se ocultan del nombre accesible. Mantiene teclado, foco y desplazamiento del desplegable mediante la primitiva existente. No conoce entidades ni endpoints. Las plantillas de activos y recursos reutilizan el mismo componente y aportan sus iconos Lucide, traducciones y validación de claves.
Consulta compacta de plantillas
Las fichas de plantillas de activos y recursos usan FieldSections layout="rows" y TemplateFieldSummary. Secciones plegables abiertas inicialmente, con títulos en mayúsculas de tamaño pequeño y color muted-foreground, sin tarjetas ni rellenos anidados. El indicador ocupa una anchura fija y los campos se alinean con el inicio del texto del título; filas compactas con nombre, tipo/unidad y obligatoriedad. Los campos desactivados conservan su aviso y los campos sin sección siguen visibles. En pantallas amplias se muestran dos columnas de secciones; en anchos menores, una. Cada fila adapta sus metadatos al ancho real de su sección.
La ayuda de consulta usa ContextHelp variant="plain": separación superior discreta, sin caja exterior ni fondo en el desplegable. Los formularios mantienen la presentación existente por defecto. FormDialog.title admite contenido React para colocar un icono decorativo junto al título sin una segunda cabecera repetida.
Formulario compacto de activos
El alta y la edición de activos usan FormSection variant="plain" para separar identificación y características mediante títulos pequeños en mayúsculas y color muted-foreground, sin tarjetas anidadas. Nombre, plantilla, estado y sede comparten una rejilla adaptable.
Para formularios que muestran todos los grupos juntos, FieldSections layout="form" presenta cada grupo plegable de ancho completo, abierto inicialmente, con separador tenue bajo el título. Los controles se alinean con el texto del título y pasan de una a dos, tres o cuatro columnas según el ancho real del contenedor, conservando el orden de los campos. No se emparejan grupos de distinta altura, evitando huecos entre secciones. Los campos sin grupo permanecen visibles; los grupos con controles inválidos se abren para permitir su enfoque.
La ayuda inferior utiliza ContextHelp variant="plain". Guardar y Cancelar permanecen en el pie fijo del diálogo; el cuerpo conserva desplazamiento cuando sea necesario. Las variantes anteriores continúan disponibles para los demás formularios. Esta distribución no modifica validación, instantáneas, valores ni operaciones de alta relacionada.
Navegación entre grupos de características
El formulario de activos mantiene nombre, plantilla, estado y sede siempre visibles en su cabecera; notas y ayuda permanecen después de las características. TabbedFieldSections de @repo/rdx-ui presenta una pestaña por grupo configurado en la plantilla, omitiendo grupos vacíos y conservando los campos sin grupo. No introduce categorías fijas de Metis Desk.
En escritorio, los grupos se navegan en una columna vertical a la izquierda de los controles. En móvil, la lista pasa a horizontal con desplazamiento; la orientación de teclado también se adapta mediante las primitivas Tabs de shadcn/ui. Los paneles se mantienen montados para conservar los valores; al cambiar de plantilla, se inicia la navegación en su primer grupo.
Antes del envío, el formulario localiza el primer control inválido y solicita la validación nativa. El panel correspondiente se activa de forma síncrona antes de que el navegador enfoque el control. Se mantienen la validación de esquema, los errores, la ayuda inferior y el pie fijo de operaciones.
Edición de plantillas globales
Los catálogos de activos y recursos ofrecen Editar para las plantillas y campos globales de serie desde el ámbito global, reutilizando sus formularios modales. En una compañía, las plantillas globales se consultan, se copian o se activa/desactiva su disponibilidad; la edición del global se realiza desde su catálogo. Los campos de serie admiten edición con tipo fijo y opciones ampliables; los tipos de relación de serie siguen protegidos.
La edición conserva UUID, clave de serie y las identidades de asociaciones que continúan. La carga inicial no sobrescribe plantillas existentes, incluidos grupos vacíos e iconos retirados. Los activos y recursos creados conservan sus instantáneas: la modificación de una plantilla se aplica a futuras altas. El uso especializado de una plantilla de recurso permanece fijo para conservar sus integraciones.
Texto de ayuda de campos de activos y recursos
El atributo description de las definiciones de campo se presenta como «Texto de ayuda», con una explicación de dónde aparecerá. En los formularios de alta y edición se muestra bajo el control, con tipografía pequeña, color muted-foreground y saltos de línea conservados. Se reutiliza FieldDescription de shadcn/ui y se vincula mediante aria-describedby, incluidos desplegables y el selector de marca.
La ayuda se consulta en el catálogo de cada ámbito por el identificador del campo, manteniendo separados activos y recursos. En edición se utiliza la ayuda vigente del catálogo sin modificar los tipos, etiquetas ni valores de la especificación capturada. Actualizar un texto de ayuda no requiere alterar datos ni historial de los activos o recursos existentes. Si el texto está vacío o falta la definición, no se muestra un bloque vacío.
Las fichas de activos usan también FieldSections layout="rows" values. Sus grupos y los de las plantillas se componen con DetailSection: título plegable en mayúsculas, pequeño y muted-foreground, indicador y contenido con sangría simétrica. DetailField destaca el valor y atenúa la etiqueta; TemplateFieldSummary destaca el nombre del campo y atenúa los metadatos. La ficha de valores mantiene una columna de grupos, adaptándose al ancho del panel lateral.
El panel lateral de activos muestra plantilla, estado y puestos activos inmediatamente después del título y antes de las acciones, mediante AssetPanelSummary. Los mismos datos no se repiten en los grupos inferiores; la ficha completa conserva sus secciones. Los enlaces mantienen su estilo común y los iconos decorativos tienen etiquetas textuales accesibles.
Ficha de puesto de trabajo
La ruta /companies/:companyId/workstations/:id muestra una ficha completa al pulsar el nombre en el listado. La cabecera concentra nombre, estado, sede, persona asignada y edición modal. El contenido adapta sus dos columnas a una en pantallas pequeñas: activos del puesto, perfiles de usuario y personas relacionadas con avatar. Los enlaces abren fichas reales de activos, personas y perfiles. El historial sigue disponible desde las acciones del listado.
Los perfiles se deducen por el ordenador principal del puesto; no se introduce una relación de propiedad entre perfil y puesto. Los inactivos se identifican explícitamente. Las personas se reúnen sin duplicados a partir de la asignación al puesto y sus perfiles. Los fallos parciales al cargar perfiles no impiden consultar los activos; se ofrece reintento. No se confunde un error o carga pendiente con ausencia de perfiles.
Gestión de usuarios
/settings/users muestra el listado de usuarios como pantalla dentro del espacio global, con acceso directo desde la navegación para el administrador inicial. Identity aporta el contenido administrativo al shell mediante composición; Companies no importa el módulo Identity. Se reutiliza UserManagement, su API y los formularios modales existentes para crear, editar y restablecer contraseñas. La activación/desactivación se mantiene en la edición; las personas de las compañías son independientes de estas cuentas. El administrador inicial sigue protegido y la autorización del backend permanece obligatoria.
Alta de recursos por pasos
El diálogo «Nuevo recurso» reutiliza FormDialog, FormBody, FormActions, FormSection y
los controles actuales. Su indicador muestra los tres pasos, identifica el actual mediante
aria-current="step" y anuncia el avance con un texto traducido al que se lleva el foco.
Las acciones conservan Cancelar y añaden Anterior/Siguiente; Guardar aparece en el último
paso. La ayuda permanece al final del cuerpo, inmediatamente antes del pie.
ResourceIdentityFields y ResourceFields comparten identificación, integración y
características entre alta y edición. ResourceFormActions encapsula las acciones de ambos
modos. Estos componentes pertenecen a resources porque conocen sus contratos y textos; no
se añade una primitiva genérica de wizard sin otros consumidores. Véase
el comportamiento del alta.
Presentación compacta del alta por pasos
El alta de recursos usa FormDialog size="medium" para limitar el ancho a 768 px;
la edición completa conserva el tamaño grande. La cabecera aporta un icono decorativo de
recurso. El indicador numera los pasos, distingue el actual y marca los completados con
un símbolo y texto accesible; las etiquetas permanecen visibles también en móvil.
Cada paso incluye una instrucción breve y, tras elegir la plantilla, muestra su nombre.
Los formularios de recursos reutilizan el patrón compacto de activos:
FormSection variant="plain", títulos pequeños en mayúsculas y color
muted-foreground, FieldSections layout="form" para las características y
ContextHelp variant="plain" al final. Se evitan tarjetas anidadas y títulos grandes
entre controles. Las columnas responden al ancho del contenedor; nombre y estado
comparten fila cuando hay espacio. Se conservan los márgenes del anillo de foco de
FormBody, la ayuda de campo, el desplazamiento del cuerpo y las acciones fijas.
Se emplean los tokens semánticos del tema, sin estilos ni cambios en vendor.
Progreso de formularios y alta de puestos
FormProgress de @repo/rdx-ui/components/form-progress presenta pasos numerados, marca el actual con aria-current="step", identifica los completados y lleva el foco al anuncio traducido de progreso. Recibe etiquetas, descripción y textos accesibles del consumidor; no conoce entidades ni consultas. Lo usan los asistentes de recursos y puestos.
El nuevo puesto se organiza en sede/responsable, activos, ordenador principal y resumen. Los controles de asociaciones, composición, ordenador y resumen tienen responsabilidades propias y se reutilizan en la edición cuando corresponde. Las acciones Anterior/Siguiente conservan valores; Guardar aparece en el resumen. La sede única y el activo único tienen selección automática. Véase Puestos de trabajo.
Selección de activos en diálogo
AssetSelect, contrato público de @metis/assets/client, sustituye los desplegables de activos de formularios. Su disparador muestra el valor y conserva la acción de alta rápida. El modo único recibe un UUID y devuelve un UUID; el múltiple recibe una colección, devuelve una colección confirmada y declara maxSelection. Los consumidores aportan opciones permitidas, bloqueos, textos y sedes. Las reglas de negocio siguen en cada formulario.
AssetPickerDialog carga inventario y catálogo mediante sus queries existentes. Conserva una selección provisional hasta confirmar; Cancelar, Escape y cerrar no modifican el formulario padre. No invoca endpoints de escritura. El diálogo muestra estados de carga/error y reintento, contador de seleccionados, controles accesibles y confirmación deshabilitada si hay selecciones inválidas o se supera el límite. Las selecciones sobreviven a búsqueda, pestañas y paginación. No navega ni cambia filtros URL de la pantalla subyacente.
AssetList, en assets/shared/ui, concentra las pestañas por UUID de plantilla (incluidas vacías/inactivas y especificaciones históricas), búsqueda rápida por nombre/plantilla/estado/sede y las columnas Nombre, Plantilla, Estado y Sede. Tanto inventario como selector lo componen con EntityTable. El inventario aporta enlaces, acciones y resaltado; el selector aporta checkboxes o radios de shadcn/ui en la primera columna de selección. Se reutilizan ordenación, columnas configurables, rangos y paginación existentes. Este componente conoce activos y permanece en su módulo; EntityTable, FormDialog, FormBody, FormActions y ButtonGroup siguen siendo los patrones genéricos independientes del dominio.
El inventario no selecciona ningún activo al entrar en el listado. Pulsar el nombre o una zona
sin controles de la fila abre su ficha lateral (en móvil, el diálogo existente), conservando
la pestaña y la búsqueda en la URL. El nombre mantiene su enlace para navegación por teclado;
los menús y demás controles conservan su acción propia. Una URL de activo concreto abre su ficha.
EntityTable.onRowClick permite esta interacción desde el consumidor sin conocer rutas o entidades.
Bajo el nombre, inventario y selector muestran un resumen automático: los cuatro primeros
campos con valor de la especificación capturada del activo, en el orden de su plantilla.
Incluye los valores y sus unidades, conserva cero y valores booleanos traducidos y omite vacíos y texto
largo. Cada tipo utiliza así sus propios campos, incluidos los personalizados, sin reglas
basadas en nombres ni cambios de persistencia. Si no hay características, se muestra solo el
nombre. El resumen ocupa como máximo dos líneas y su texto completo está disponible en title.
El selector se aplica a activos del puesto (adición múltiple), ordenador principal (único entre miembros), extremo de relaciones, impresora por perfil, origen/destino de conexiones remotas. Cada caso conserva exclusiones, referencias fijas y altas rápidas. Añadir varios activos a un puesto es una sola actualización de composición; solo se propone automáticamente el ordenador cuando la composición resultante contiene un único activo.
Panel lateral y resumen de recursos
Recursos aplica el mismo patrón de selección que activos: el listado comienza sin ficha, el nombre y las zonas sin controles de cada fila abren el recurso y conservan la pestaña y la búsqueda de la URL. La selección se resalta mientras el panel está abierto. El cierre oculta el panel y ofrece recuperarlo desde la cabecera. Las URLs de un recurso concreto siguen abriendo su ficha; en pantallas pequeñas se conserva el diálogo con historial.
SelectionPanel de @repo/rdx-ui/components/selection-panel comparte la estructura de
activos y recursos: título, cierre accesible, resumen, acciones y contenido por composición.
El consumidor aporta textos traducidos, datos y callbacks. useCompactDetail comparte el
umbral de 1100 px con la variante inventory de Tailwind. Las fichas completas conservan
su diálogo; el panel de recursos muestra características, integración, notas y contenido
relacionado, sin repetir el historial completo.
characteristicsSummary de @repo/rdx-ui/lib/characteristics-summary recibe campos,
valores y textos de Sí/No sin depender de DTOs. Activos y recursos muestran los primeros
cuatro valores no vacíos en el orden de su especificación, con unidades, cero y booleanos
traducidos. El resumen ocupa dos líneas bajo el nombre y ofrece el texto completo en
title. No se añaden reglas basadas en nombres de plantillas ni cambios de persistencia.
Componentes archivados
Las referencias históricas a formularios y listados de tickets, intervenciones y sus plantillas pertenecen al archivo de funcionalidades. No forman parte de la aplicación activa.
Formulario de personas
El alta y la edición comparten PersonForm y PersonEditorDialog. Los switches de
persona activa y contacto de la compañía aparecen al comienzo, en una franja adaptable.
Los datos personales y de contacto se agrupan con FormSection variant="plain" y
rejillas de una o dos columnas. La ayuda contextual permanece al final del cuerpo.
PersonPhotoField encapsula el selector accesible de foto y su vista previa circular,
utilizando Avatar y Button de shadcn/ui. Sin imagen se muestra un placeholder de cámara;
al editar se presenta la foto vigente. La selección es provisional hasta guardar y las
URLs de vista previa se liberan al cambiar de archivo o desmontar el componente.
Jerarquía del editor de plantillas
El alta y la edición de plantillas de activos y recursos agrupan los datos generales con FormSection e incorporan campos desde las cabeceras de sus grupos. TemplateSectionsEditor permite secciones apiladas o una cuadrícula responsive mediante sectionLayout="stack" | "responsive", con una cabecera numerada sobre fondo diferenciado que contiene el input del nombre, el asa de arrastre y las acciones de orden y eliminación. El cuerpo conserva la descripción y un área de campos separada por un borde. TemplateSectionEditor encapsula cada sección con Collapsible: todas empiezan abiertas y un botón de cabecera permite plegarlas con ratón o teclado. El nombre, el asa y las acciones siguen visibles al plegar. El contenido permanece montado para conservar los controles; si la validación nativa detecta un control inválido oculto, la sección se abre antes de que el navegador lo enfoque. La apertura se conserva por identidad al reordenar y no se guarda como parte de la plantilla. Los editores de activos y recursos usan la cuadrícula responsive: una columna hasta 1.000 px de ancho disponible y dos a partir de ese umbral, con separación de 16 px y tarjetas de igual altura en cada fila, determinada por la sección más alta. El orden recorre cada fila de izquierda a derecha y se conserva en el DOM, la numeración, el teclado y el arrastre; la altura de cada fila se recalcula al plegar o desplegar sus secciones, manteniendo ambas tarjetas con la misma altura. El bloque «Sin sección» permanece debajo, a ancho completo.
TemplateSectionFields muestra un título traducido y el número de campos. Cada campo tiene fondo propio. En la presentación cards, separa su nombre y acciones de la configuración de obligatoriedad y asignación, cuyo selector conserva una etiqueta visible; los editores de plantillas de activos y recursos utilizan la presentación compact descrita más abajo. Los campos se distribuyen en dos columnas a partir de 700 px de su propio grupo y en una para anchos menores; cada grupo establece su contenedor para evitar columnas demasiado estrechas cuando las secciones se muestran en paralelo. El bloque «Sin sección» se diferencia con borde discontinuo; las secciones vacías conservan un mensaje explícito. Se reutilizan los colores semánticos del tema para mantener el contraste en modo claro y oscuro.
Orden mediante arrastre y teclado
TemplateSectionsEditor y TemplateSectionFields utilizan useDragReorder y ReorderHandle, independientes del dominio. Cada sección y campo incorpora un asa etiquetada, con ayuda traducida y posición actual en su nombre accesible. Se puede arrastrar desde el asa y soltar en la mitad superior o inferior de otro elemento para colocarlo antes o después; una línea de color primario marca el destino sin modificar la altura del contenido. El elemento de origen se atenúa durante el arrastre. El orden se confirma únicamente al soltar y se conserva hasta guardar el formulario; cancelar el arrastre no cambia los datos.
El arrastre utiliza la API HTML nativa, sin dependencias adicionales. Cada lista tiene un ámbito propio: las secciones se reordenan entre sí y los campos dentro de su sección, incluido el bloque «Sin sección». En la presentación de tarjetas, el selector permite trasladar un campo a otro grupo. No se aceptan arrastres externos ni de otro diálogo. Las teclas arriba y abajo sobre el asa permiten mover el elemento conservando el foco; los botones explícitos de orden siguen disponibles, también para dispositivos sin arrastre nativo. Las asas quedan deshabilitadas cuando se guarda o no hay otro elemento en su lista.
Selección múltiple de campos de plantillas de activos y recursos
El alta y la edición de plantillas de activos y recursos abren el CatalogFieldPickerDialog de su módulo desde «Añadir campo» en las cabeceras de los grupos de campos, siguiendo el patrón de AssetPickerDialog y AssetPickerList: FormDialog, FormBody, FormActions, EntityTable y Checkbox. La tabla reutiliza búsqueda, ordenación, columnas y paginación, con nombre, descripción, tipo y unidad como contexto. Solo recibe campos activos del catálogo del formulario que todavía no están en la plantilla; no añade consultas ni dependencias.
La selección permanece al buscar o paginar y se confirma como un lote mediante «Añadir campos». Cancelar descarta la selección provisional. Los campos nuevos se añaden al final, como opcionales en la sección de destino («Sin sección» desde el grupo sin sección), conservando el orden, agrupación y obligatoriedad de los existentes. appendTemplateFields deduplica y valida el conjunto con el schema existente; las casillas impiden superar su límite. El alta rápida «Nuevo campo» se ofrece en el pie de CatalogFieldPickerDialog, a la izquierda, mediante el slot leadingActions de FormActions. La prop opcional quickCreate aporta el ámbito y permite deshabilitar el botón con disabled; omitirla oculta la acción e impide abrir el alta. El editor de plantillas traduce su opción existente allowQuickCreate a este contrato. El diálogo de selección permanece montado durante el alta para conservar la búsqueda, página y selección provisional. Guardar el nuevo campo actualiza el catálogo y lo marca en esa selección; solo «Añadir campos» modifica el borrador de la plantilla. Cancelar el alta conserva la selección; cancelar el selector no elimina un campo ya creado del catálogo. Los cambios se persisten al guardar la plantilla.
Interacción de filas en PickerList
En todos los diálogos de tipo PickerList, la columna del checkbox o radio ocupa la primera posición, antes del nombre y los demás datos. Usa el identificador selection, no se ordena ni se oculta y no se ancla a la derecha como las acciones. EntityTable oculta visualmente su cabecera mediante sr-only, conservando el nombre accesible; ajusta su anchura al control y reduce el relleno derecho para acercarlo al nombre. Se conserva el ancho mínimo de la columna del nombre. Los avisos de indisponibilidad de activos se muestran junto al nombre, para no ensanchar la columna de selección. AssetList permite anteponerla mediante leadingColumns, manteniendo las acciones adicionales al final. Pulsar una zona no interactiva de una fila disponible cambia su selección: alterna el checkbox en modo múltiple y selecciona el radio en modo único. AssetPickerList y CatalogFieldPickerList reutilizan onRowClick de EntityTable (a través de AssetList cuando corresponde), que ya excluye clics en controles internos. Así, pulsar directamente una casilla o un radio produce un único cambio y mantiene el uso por teclado. Las filas deshabilitadas respetan el mismo bloqueo que su control y muestran el cursor de acción no disponible. Esta regla se aplica también a los nuevos pickers.
Altura estable de los diálogos PickerList
Todos los diálogos PickerList usan FormDialog layout="picker". Esta variante fija la altura a la ventana disponible en móvil, con un margen total de 2 rem; en escritorio utiliza el menor valor entre el 85 % de la altura dinámica de la ventana y 900 px, respetando siempre el margen. La altura no depende de carga, errores, búsqueda, número de resultados o tamaño de página. Los formularios de alta y edición conservan la altura por contenido, también cuando se abren desde un picker.
FormBody hereda esta disposición del diálogo y distribuye sus hijos en columna. EntityTable layout="fill" ocupa el espacio restante; búsqueda, filtros, información y paginación no se desplazan con los resultados. El contenedor de tabla tiene scroll interno y scrollbar-gutter: stable; las cabeceras permanecen visibles al recorrer las filas. El área mantiene su espacio con pocos resultados o ninguno. En ventanas pequeñas, el cuerpo también puede desplazarse para evitar recortar controles. AssetList propaga layout="fill" por las pestañas y AssetPickerList conserva ese comportamiento en selección única y múltiple. AssetPickerDialog y CatalogFieldPickerDialog aplican esta variante común.
Incorporar campos desde una sección de plantilla
En los editores de plantillas de activos y recursos, la cabecera «Campos» de cada sección ofrece «Añadir campo». Reutiliza CatalogFieldPickerDialog y muestra la sección de destino; permite seleccionar campos existentes o usar el alta rápida del pie cuando está habilitada. Al confirmar, solo las referencias nuevas se añaden como opcionales a ese grupo; no mueve ni cambia la obligatoriedad de los campos existentes. El bloque independiente «Añadir campos a la plantilla» se ha retirado. Si todavía no existen secciones, se muestra el grupo «Campos» con su botón de incorporación y ayuda de estado vacío, cuyo destino es «Sin sección». Cancelar no cambia el borrador.
TemplateSectionsEditor recibe la acción opcional addFieldAction, que traslada el destino al módulo consumidor. TemplateSectionFields presenta esa acción en su cabecera, con el contexto accesible del grupo y bloqueo durante el guardado; también se puede reutilizar en «Sin sección» cuando está visible. Los componentes genéricos no conocen el selector ni sus DTO. Cada módulo conserva el destino por identidad mientras el picker está abierto, comprueba su pertenencia a la plantilla y reutiliza appendTemplateFields para deduplicar y validar el lote.
Selector de campos compartido
FieldPickerDialog, FieldPickerList y useFieldPickerController, en @repo/rdx-ui, comparten la selección múltiple, confirmación, alta opcional y bloqueo por capacidad entre activos y recursos. La tabla recibe datos presentacionales (identidad, nombre, descripción, tipo traducido y unidad) y etiquetas. El diálogo recibe la tabla y el alta mediante composición, sin importar DTO, consultas, schemas ni endpoints de los módulos. Los adaptadores CatalogFieldPickerDialog y CatalogFieldPickerList de cada módulo aportan esos datos, sus traducciones y su formulario de alta.
appendTemplateBindings concentra la incorporación idempotente de referencias, conservando las existentes y asignando solo las nuevas al destino. Cada appendTemplateFields valida el resultado con el schema normativo de su módulo. Las plantillas de recursos mantienen su configuración de uso y las restricciones de alta rápida del consumidor.
Datos de las plantillas de activos y recursos
El alta y la edición de plantillas de activos y recursos reutilizan TemplateMetadataSection, que compone FormSection variant="plain" para «Datos de la plantilla», sin borde ni relleno interior de tarjeta. Los controles se distribuyen en dos columnas iguales a partir de 560 px del contenedor: nombre e icono debajo en la primera, descripción en la segunda. Ambas columnas tienen la misma altura; el área de descripción crece para alinearse con el borde inferior del selector de icono. En espacios estrechos se apilan en el orden nombre, icono y descripción. El icono conserva una anchura máxima de 16 rem. El componente recibe título traducido, primaryFields, descriptionField y contenido adicional opcional mediante children, sin conocer datos ni formularios de los módulos. Añade 16 px a la separación base de 20 px entre grupos. Recursos conserva su selector de uso y su ayuda bajo las dos columnas.
Títulos de grupos en diálogos
Todos los diálogos con grupos utilizan el estilo de «Datos de la plantilla»: texto pequeño, seminegrita, mayúsculas, espaciado entre letras y color atenuado (text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-4). FormSectionTitle centraliza esta presentación y recibe children, id opcional y as="h3" | "h4" | "h5" | "legend" | "span" para conservar la semántica del encabezado, leyenda o título dentro de un summary. El consumidor aporta el texto traducido; las mayúsculas se aplican mediante CSS. La variante span conserva la tipografía y deja el espaciado al summary que la contiene, sin margen inferior propio.
FormSection, TemplateSectionsEditor y TemplateSectionFields comparten este título, con independencia de sus bordes, acciones o contenido. La regla también se aplica a grupos de consulta, como los puestos del historial de perfiles. Los títulos principales del diálogo y los nombres editables de secciones mantienen su presentación propia.
Campos compactos en plantillas de activos y recursos
TemplateSectionsEditor fieldLayout="compact" presenta cada campo en una fila: asa de arrastre, switch de obligatoriedad, nombre flexible, flechas arriba/abajo y papelera. El nombre puede ocupar varias líneas en pantallas estrechas. El switch conserva una etiqueta accesible con «Obligatorio» y el nombre del campo; su texto visual y el selector de sección se omiten. Las notas de campos inactivos siguen disponibles para lectores de pantalla. Se mantienen el arrastre, las flechas por teclado, los límites de orden y los estados deshabilitados. Ambos editores utilizan esta presentación compacta.
Ajustes comunes tras la revisión de diálogos
FormBody establece su propio contenedor de consultas de tamaño: las cuadrículas de formularios
y subcontroles utilizan @min-[560px] para decidir sus dos columnas según el ancho real del
modal. FieldSections aplica la misma regla a los campos y estira los grupos de cada fila
a la altura de su vecino más alto. Los títulos de consulta, grupos de permisos y secciones
desplegables reutilizan FormSectionTitle, conservando el estilo vigente del componente.
FormActions admite cancelDisabled para conservar el bloqueo de cancelación durante una
confirmación pendiente, además de leadingActions para acciones opcionales a la izquierda.
Las confirmaciones de marcas y definiciones de catálogo usan FormBody y FormActions para
mantener su pie fuera del área desplazable. La selección de compañía del dashboard también
sitúa Continuar en ese pie y lo deshabilita mientras falta una selección o una consulta correcta.
SoftwareInstallationEditorDialog, SoftwareResourceCreateDialog y EntityTableColumnsDialog
encapsulan sus interacciones en ficheros independientes. Sus consumidores conservan apertura,
selección, datos y callbacks; los formularios y controles mantienen sus contratos anteriores.
Etiquetas y campos obligatorios
FormLabel, en @repo/rdx-ui/components/form-label, compone la primitiva Label de
shadcn/ui y conserva sus props y la asociación htmlFor. La prop required añade al
final un único asterisco con color destructive, mediante RequiredIndicator. El indicador
es visual; el control mantiene sus atributos de validación y su semántica accesible.
La etiqueta de un campo optativo contiene solo su texto, sin sufijos de opcionalidad.
Los módulos aportan nombres traducidos sin asteriscos y la condición de obligatoriedad.
Las características de activos y recursos conservan su unidad; las referencias y fechas
condicionales utilizan la condición de su contrato. No se deduce obligatoriedad de que
un valor esté vacío, de su estado deshabilitado ni de que un booleano sea false.
Los grupos de campos, como permisos, reutilizan RequiredIndicator en su leyenda,
sin marcar cada checkbox como obligatorio. La configuración de obligatoriedad de las
plantillas conserva sus switches y su significado.
TemplateFieldSummary aplica el mismo criterio en la consulta de plantillas: recibe
requiredLabel con el texto traducido de obligatoriedad solo si el campo es obligatorio.
Muestra el asterisco junto al nombre y conserva ese texto para lectores de pantalla.
Los campos optativos no muestran indicación de opcionalidad; tipo, unidad y avisos se
mantienen como metadatos de consulta.
Pestañas horizontales desplazables
ScrollableTabsList de @repo/rdx-ui/components/scrollable-tabs-list se compone dentro de Tabs de shadcn/ui. Recibe items (identidad value y etiqueta label), value, una etiqueta accesible label y locale para los controles es/en. El consumidor mantiene la selección, los paneles y sus efectos sobre URL, filtros o consultas.
Los listados de activos y recursos lo utilizan para sus familias; el picker de activos lo hereda mediante AssetList. La barra ocupa únicamente el ancho disponible, mantiene una sola fila y no comprime los nombres. Permite desplazamiento horizontal nativo con tacto o trackpad y muestra botones laterales solo cuando hay desbordamiento. Los botones se deshabilitan al llegar a cada extremo. La barra de desplazamiento nativa permanece oculta, evitando controles verticales accidentales; los botones conservan nombre accesible y tooltip.
Conserva la semántica y el teclado de Base UI. Al cambiar de familia, enfocar una pestaña o redimensionar el contenedor, revela la pestaña correspondiente moviendo solo el eje horizontal. Un ResizeObserver actualiza los extremos al abrir o cerrar paneles, redimensionar la ventana o cambiar las etiquetas; se retiran el observer y los listeners al desmontar. La raíz Tabs y los paneles consumidores mantienen min-w-0 para permitir que la barra se reduzca dentro de flex y grid.