From 6b086e17f27d9037a18fa9a64ee4c7cffcf77975 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 19:31:09 +0200 Subject: [PATCH] Add visual regression tests for UI components using Playwright - Implemented a new test suite in `visual-audit-ui.test.py` to perform visual audits on the UI. - Tests include checking row contrast, wizard geometry, focus behavior, and review headers across different screen sizes. - Utilizes Playwright for browser automation and captures visual snapshots for comparison. - Ensures that the tests run against a synthetic session state for accurate results. --- DESIGN.md | 20 +- docs/auditoria-ui-ux-visual.md | 108 ++++ docs/componentes-de-interfaz.md | 19 +- docs/propuesta-navegacion-responsive-v01.md | 476 ++++++++++++++++++ .../src/web/assets/list/ui/inventory-page.tsx | 50 +- .../src/web/assets/shared/inventory.test.tsx | 4 +- .../assets/shared/ui/asset-editor-dialog.tsx | 4 + modules/companies/src/common/locales/en.json | 1 + modules/companies/src/common/locales/es.json | 1 + .../ui/application-configurations-page.tsx | 2 +- .../shared/application.test.tsx | 9 + .../ui/certificate-configurations-page.tsx | 2 +- .../shared/certificate.test.tsx | 9 + .../list/ui/folder-mappings-page.tsx | 2 +- .../list/ui/mail-configurations-page.tsx | 2 +- .../mail-configurations/shared/mail.test.tsx | 9 + .../list/ui/printer-configurations-page.tsx | 2 +- .../shared/printer.test.tsx | 9 + .../list/ui/work-profiles-page.tsx | 2 +- .../web/work-profiles/shared/profile.test.tsx | 48 ++ .../shared/ui/resource-editor-dialog.tsx | 1 + .../rdx-ui/src/components/form-dialog.tsx | 6 +- .../rdx-ui/src/components/form-progress.tsx | 4 +- .../src/components/form-step-actions.tsx | 9 +- .../rdx-ui/src/selection-components.test.tsx | 8 +- scripts/visual-audit-ui.test.py | 196 ++++++++ 26 files changed, 971 insertions(+), 32 deletions(-) create mode 100644 docs/propuesta-navegacion-responsive-v01.md create mode 100644 scripts/visual-audit-ui.test.py diff --git a/DESIGN.md b/DESIGN.md index 4881f28..1c2c5b0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -127,7 +127,7 @@ Usar el [catálogo semántico propio](packages/rdx-ui/src/lib/icons.ts), basado `apps/web/components.json` declara Tabler; el vendor conserva Lucide en su configuración e iconos internos. No migrar vendor ni importar bibliotecas de iconos directamente desde módulos. -Button ofrece default/outline/secondary/ghost/destructive/link. Conservar sus hover, expanded, disabled y focus-visible; disabled reduce opacidad y bloquea interacción. Input conserva ring de foco y estado inválido. `.app-link` usa primario, subrayado en hover y outline de 2 px con offset de 3 px en foco. Las combinaciones de selección deben comprobarse: V02 sigue abierto. No se añaden tokens ni variantes nuevas en esta versión documental. +Button ofrece default/outline/secondary/ghost/destructive/link. Conservar sus hover, expanded, disabled y focus-visible; disabled reduce opacidad y bloquea interacción. Input conserva ring de foco y estado inválido. `.app-link` usa primario, subrayado en hover y outline de 2 px con offset de 3 px en foco. Las combinaciones de selección deben comprobarse: V02 está corregido en el inventario claro verificado, con primario, texto secundario y ring contextuales limitados a sus filas. No cambia la paleta global ni añade tokens. ## 4. Layout y navegación @@ -182,7 +182,7 @@ Fuente normativa: [patrón de diálogos](docs/patron-de-dialogos.md). Cada inter | Capacidad implementada | Uso y adopción pendiente | | --- | --- | | FormDialog small/medium/large | Desde sm: máximos 36 rem/48 rem; large 90vw con máximo 1200 px. Margen y altura máxima respecto a ventana. Consultar su [implementación](packages/rdx-ui/src/components/form-dialog.tsx). | -| `layout="stable"` | Para contenido variable, pasos, filtros y selecciones: altura `calc(100dvh - 2rem)`; desde sm, `min(85dvh,900px)`. Puestos/perfiles lo utilizan; activos/recursos todavía incumplen esta pauta, V03. | +| `layout="stable"` | Para contenido variable, pasos, filtros y selecciones: altura `calc(100dvh - 2rem)`; desde sm, `min(85dvh,900px)`. Puestos/perfiles lo utilizan; activos/recursos lo adoptan en el sprint 1, con Anterior deshabilitado en el primer paso para estabilizar también el pie móvil (V03 verificado). | | `layout="picker"` | Selector de altura estable; búsqueda inicial en input `data-quick-search`; tabla `layout="fill"`, selección primera y compacta, confirmación/cancelación de borrador. | | `layout="content"` | Diálogos breves de contenido estable; es el default técnico, no la elección normativa para asistentes variables. | | FormBody | Scroll interno, `min-h-0`, margen de foco; `contentLayout="fill"` distribuye alto restante y permite que EmptyState crezca por toda la cadena flex. | @@ -193,7 +193,7 @@ Etiquetas con `htmlFor`, obligatoriedad según contrato y un único indicador de Una fuente de verdad por campo; React Hook Form y schemas Zod según patrones existentes. Errores junto al campo o como error global según su origen, con asociación accesible para mensajes propios; no cerrar ni limpiar antes de confirmar guardado. La ayuda no rellena campos automáticamente; propuestas de nombre o selecciones únicas solo donde el contrato específico ya lo establece. -Conservar cabecera y acciones visibles, scroll del cuerpo y valores al retroceder. Guardar al final del asistente; impedir envíos duplicados. Cancelar/Escape/cierre descartan el borrador según la operación y restituyen foco al disparador; **V06** evidencia un consumidor que no lo cumple. No añadir manualmente primitivas de modalidad que Base UI ya resuelve. +Conservar cabecera y acciones visibles, scroll del cuerpo y valores al retroceder. Guardar al final del asistente; impedir envíos duplicados. Cancelar/Escape/cierre descartan el borrador según la operación y restituyen foco al disparador; **V06** se corrigió en el inventario mediante finalFocus de Base UI: disparador capturado en la apertura y destino alternativo local cuando no está disponible. No añadir manualmente primitivas de modalidad que Base UI ya resuelve. En diálogos anidados, mantener borrador padre y foco de retorno; cancelar selección no modifica el padre. Un alta rápida guardada puede persistir aunque después se cancele el formulario principal: respetar su contrato, no tratarla como una selección provisional. No añadir niveles de anidación sin revisar los consumidores existentes. @@ -212,7 +212,7 @@ En diálogos anidados, mantener borrador padre y foco de retorno; cancelar selec | Valor ausente | En toda celda de listado/sublistado/PickerList, exclusivamente «—»; conservar 0 y false con su representación correcta. | | Estado de negocio | Texto real como Inactivo o Sin puesto activo no equivale automáticamente a ausencia. «Sin asignar» puede ser placeholder de un selector; en una celda que solo carece de sede corresponde «—». V11 requiere revisar renderizadores. | -Textos de interfaz es/en mediante i18n; código e identificadores en inglés, documentación en castellano. Etiquetas breves para columnas/acciones, explicación en ayuda contextual. V09 señala una traducción explicativa reutilizada como cabecera; no debe convertirse en patrón. No deducir permisos ni estado de una entidad solo por color. +Textos de interfaz es/en mediante i18n; código e identificadores en inglés, documentación en castellano. Etiquetas breves para columnas/acciones, explicación en ayuda contextual. V09 se corrigió separando profiles.reviewLabel (Revisión/Review) de los motivos completos conservados en las celdas. No deducir permisos ni estado de una entidad solo por color. ## 9. Accesibilidad verificable @@ -222,7 +222,7 @@ Aplicar las obligaciones de AGENTS y de los patrones; comprobar comportamiento e - Teclado sin operaciones exclusivas de ratón/arrastre, orden DOM coherente, Tab/Shift+Tab, Enter/Espacio y Escape según la primitiva. No bloquear controles internos de filas. - Foco visible sin recortes; inicial pertinente, contenido inválido accesible y retorno al origen al cerrar. PickerList busca por defecto. Verificar cada consumidor, no presumir cobertura general por usar Dialog. - Validación HTML nativa conserva su semántica; su ausencia de `aria-invalid` no prueba un defecto. Para errores propios, asociar mensajes y estado al control, abrir grupos inválidos y diferenciar error global. -- Medir colores computados de texto/fondo efectivo y estados. Referencia [WCAG 2.2, 1.4.3](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html): 4,5:1 texto normal, 3:1 texto grande según su definición. V02: enlace de activo sobre selección ≈2,68:1, pendiente de corrección. Blanco sobre el botón primario medido ≈4,65:1; no extender el fallo a todos sus usos. +- Medir colores computados de texto/fondo efectivo y estados. Referencia [WCAG 2.2, 1.4.3](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html): 4,5:1 texto normal, 3:1 texto grande según su definición. V02: el enlace sobre selección pasó de ≈2,68:1 a ≈7,81:1 en el sprint 1; normal ≈4,65:1 y hover sin selección ≈12,86:1, con foco visible comprobado. Blanco sobre el botón primario medido ≈4,65:1; no extender el fallo a todos sus usos. - Comprobar zoom 200/400 %, acceso a contenido/acciones, contraste de estados y respeto a movimiento reducido; conservar las variantes `motion-reduce` existentes. Estas comprobaciones, lector de pantalla y modo oscuro no se validaron completamente en la auditoría. No declarar cumplimiento WCAG global a partir de tokens, snapshots, capturas o una muestra de contraste. @@ -249,19 +249,19 @@ Procedimiento: [Playwright MCP](docs/playwright-mcp.md), [preparador sintético 4. Esperar carga y transiciones antes de medir. Abrir los PNG: captura, snapshot, geometría y prueba funcional son evidencias distintas. Las capturas en `.artifacts/playwright/`, excluidas de Git, **no son evidencia portable por sí solas**: acompañar el informe con escenarios, medidas y un mecanismo explícito de entrega de imágenes sin secretos. 5. Ejecutar pruebas de comportamiento y comprobaciones apropiadas al alcance según AGENTS; cambios transversales de código requieren typecheck, lint, test, check y build. Para documentación, enlaces, tokens y diff bastan; informar lo ejecutado y lo pendiente, sin afirmar validaciones no realizadas. -Todos los hallazgos siguientes están **pendientes**; las mediciones detalladas y pasos viven en la auditoría: +V02, V03, V06 y V09 están **resueltos en los escenarios verificados del sprint 1**; los otros ocho hallazgos siguen pendientes. Las mediciones originales y el seguimiento posterior se conservan en la auditoría: | ID | Tipo y alcance | Próxima decisión o comprobación | | --- | --- | --- | | V01 | Diseño de layout/navegación + implementación | Diseñar por capacidad real; validar móvil, tablet y tabla/panel antes de fijar nuevos umbrales | -| V02 | Combinación de tokens/estados y consumidor | Corregir contraste de selección; medir pares efectivos y hover | -| V03 | Adopción de patrón existente | Aplicar estabilidad a asistentes de activos/recursos; probar pasos cortos/largos | +| V02 | Resuelto: combinación de tokens/estados del inventario claro | Contraste y foco comprobados; no equivale a validar todos los temas o tablas | +| V03 | Resuelto: adopción de patrón y pie estable | Tres pasos de activos/recursos verificados en los cuatro tamaños del sprint | | V04 | Diseño de ayuda/progreso compacto + componentes | Acordar presupuesto de altura sin perder ayuda junto al pie; verificar 320×640 | | V05 | Estado/navegación de implementación | Definir restauración completa por vista; orden reproducido, página de activos pendiente por datos | -| V06 | Integración de foco del diálogo | Reproducir y corregir retorno con Escape/Cancelar y distintos disparadores | +| V06 | Resuelto: integración de foco del inventario | Ratón/Enter, Escape/Cancelar, cabecera/fila, alternativa y selector anidado verificados | | V07 | Contrato de búsqueda y consumidores | Acordar normalización por idioma y probar tildes/ñ; no confundir con ordenación | | V08 | Evolución del selector de columnas | Diseñar búsqueda/agrupación con API genérica; conservar preferencias | -| V09 | Etiquetas/i18n de listados | Separar cabecera breve de motivo explicativo y revisar casos equivalentes | +| V09 | Resuelto: etiquetas/i18n de seis listados | Cabeceras breves y motivos conservados; tests es/en y navegador | | V10 | Jerarquía de ficha | Diseñar resumen de vacíos y grupos sin ocultar relaciones/acciones; validar fixture poblada | | V11 | Renderizadores y semántica de ausencia | Aplicar pauta de «—» distinguiendo estado de negocio | | V12 | Carga inicial de implementación | Reservar estructura y mensajes precisos; probar recuperación y actualización | diff --git a/docs/auditoria-ui-ux-visual.md b/docs/auditoria-ui-ux-visual.md index 407696f..8a3cbc2 100644 --- a/docs/auditoria-ui-ux-visual.md +++ b/docs/auditoria-ui-ux-visual.md @@ -362,3 +362,111 @@ Las comprobaciones posteriores prevalecen sobre las observaciones transitorias: No se validaron guardados satisfactorios, estados de éxito, conflictos concurrentes, errores de servidor representativos, datos extensos, historial con múltiples modificaciones, relaciones pobladas, administración de usuarios, fichas de persona/puesto/perfil/recurso ni todos los pickers. Tampoco se validaron todos los pasos de puesto/perfil, por ausencia de sedes y puestos. Para cerrarlos hace falta una fixture ampliada y autorizada; no basta con revisar su JSX. No se ejecutaron `pnpm typecheck`, `lint`, `test`, `check` o `build`: no se ha cambiado código ejecutable. Se ejecutaron el preparador autenticado, los recorridos MCP descritos, comprobaciones de capturas y enlaces locales del informe y revisión del diff. Al finalizar se cerró el preparador: retiró su entorno temporal y se comprobó la eliminación de `session.json` y `database.env`. Las capturas se conservan sin la sesión privada. + +## 12. Seguimiento — UI/UX Improvement Sprint 1 + +Verificación posterior del 21 de octubre de 2026. Se conservan íntegros los hallazgos y +mediciones originales de las secciones anteriores. Este seguimiento cambia únicamente +el estado de **V02, V03, V06 y V09**, dentro de los escenarios comprobados. V01, V04, +V05, V07, V08, V10, V11 y V12 continúan pendientes. + +### Resultado por hallazgo + +| Hallazgo | Estado posterior | Cambio y comprobación | +| --- | --- | --- | +| V02 | Resuelto en el inventario claro comprobado | Primario, texto secundario y ring contextuales mediante accent-foreground en la fila seleccionada y en hover, sin cambiar tokens globales. Enlace de nombre y Crear puesto pasan de 2,68:1 a **7,81:1** sobre selección. Enlaces en estado normal sobre blanco: **4,65:1**; hover sin selección: **12,86:1**. Incluyendo el texto secundario, el mínimo medido en hover sin selección es **4,59:1**, y en selección **7,81:1**. Acciones fijadas legibles y foco visible con Tab/Shift+Tab. No es una declaración WCAG global ni una validación de modo oscuro. | +| V03 | Resuelto en ambos asistentes y cuatro tamaños | AssetEditorDialog y ResourceEditorDialog usan stable. Anterior permanece deshabilitado en el paso inicial para que el pie móvil no cambie de número de filas. El anuncio de progreso usa preventScroll y el cambio de paso devuelve el cuerpo a su inicio. Los tres pasos mantienen altura, cabecera y coordenada del pie, con cuerpo desplazable y Escape funcional. | +| V06 | Resuelto en los disparadores comprobados del inventario | FormDialog reexpone finalFocus de Base UI. El consumidor captura el disparador antes del foco del anuncio y aporta destino alternativo si desaparece o se deshabilita. Ratón/Enter, Escape/Cancelar, cabecera/fila y selector anidado probados; sin retorno a BODY. | +| V09 | Resuelto en seis listados | Cabecera independiente profiles.reviewLabel: Revisión/Review. Se mantienen las explicaciones completas y condiciones de revisión en las celdas de perfiles, carpetas, aplicaciones, certificados, impresoras y correo. | + +### Geometría antes y después + +Valores en píxeles CSS, con fuentes y transiciones asentadas. La tabla muestra las +alturas de los pasos 1/2/3; dentro de cada fila posterior el pie conserva la misma y. + +| Resolución | Activos antes | Activos después | Recursos antes | Recursos después | +| --- | --- | --- | --- | --- | +| 1440×900 | 615 / 868 / 868 | 765 / 765 / 765 | 615 / 797 / 777 | 765 / 765 / 765 | +| 1366×768 | 615 / 736 / 736 | 652,8 / 652,8 / 652,8 | 615 / 736 / 736 | 652,8 / 652,8 / 652,8 | +| 390×844 | 812 / 812 / 812, pie variable | 812 / 812 / 812 | 812 / 812 / 812, pie variable | 812 / 812 / 812 | +| 320×640 | 608 / 608 / 608, pie variable | 608 / 608 / 608 | 608 / 608 / 608, pie variable | 608 / 608 / 608 | + +El pie posterior comienza en y=762,5 / 640,4 / 670 / 466 respectivamente. Se comprobó +scroll interno con contenido mayor que el cuerpo disponible y acciones dentro del +viewport. El progreso sigue perteneciendo al cuerpo desplazable; no se ha rediseñado +su densidad ni la ayuda de V04, y un desplazamiento del usuario puede sacarlo de vista. + +### Evidencias antes/después y pruebas + +Carpeta independiente [ui-sprint-1](../.artifacts/playwright/ui-sprint-1/); no se han +sobrescrito las imágenes históricas de ui-audit. Imágenes representativas abiertas: + +- V02: [antes](../.artifacts/playwright/ui-sprint-1/before/contrast-selected-1440.png) y [después](../.artifacts/playwright/ui-sprint-1/after/contrast-selected-1440.png). +- V03: [activo antes](../.artifacts/playwright/ui-sprint-1/before/assets-320-step-2.png), [activo después](../.artifacts/playwright/ui-sprint-1/after/assets-320-step-2.png), [recurso después](../.artifacts/playwright/ui-sprint-1/after/resources-390-step-2.png). +- V06: [antes](../.artifacts/playwright/ui-sprint-1/before/focus-header-keyboard-Escape.png) y [después](../.artifacts/playwright/ui-sprint-1/after/focus-header-keyboard-Escape.png). +- V09: [antes](../.artifacts/playwright/ui-sprint-1/before/headers-work-profiles-1440.png) y [después](../.artifacts/playwright/ui-sprint-1/after/headers-work-profiles-1440.png). + +[visual-audit-ui.test.py](../scripts/visual-audit-ui.test.py) reutiliza el cliente MCP y +la sesión del preparador existente, sin SDK o infraestructura de navegador paralelos. +Bloquea todas las escrituras HTTP. Se ejecuta con el preparador activo: + +```bash +METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-/session.json \ +METIS_UI_AUDIT_PHASE=after python3 scripts/visual-audit-ui.test.py +``` + +La fase before conserva las reproducciones iniciales y falla las expectativas de +regresión mientras las incidencias están presentes. La verificación posterior consta +de cuatro pruebas principales correctas, ampliación de foco/cabeceras a los otros +tamaños y contraste de seis estados. Se conservan mediciones y snapshots, además de +los resultados en after/test-result.log, after/additional-result.log y +after/contrast-final-result.log y after/headers-final-result.log. measurements.json incluye pasadas intermedias; +summary-latest.json resume la última observación por escenario. El índice de PNG +contiene dimensiones y SHA-256. Las evidencias siguen excluidas de Git y requieren +entrega explícita para compartirlas. + +Se ampliaron tests es/en de cinco listados y una prueba con perfil sintético en caché +para asegurar que la cabecera breve no elimina el motivo detallado de la celda. Se +actualizó la prueba del contrato de Anterior deshabilitado en el primer paso. No se +han guardado personas, puestos, recursos ni ediciones en el entorno visual. + +### Archivos y responsabilidades + +| Área | Archivos principales | +| --- | --- | +| Inventario | [inventory-page.tsx](../modules/assets/src/web/assets/list/ui/inventory-page.tsx): color contextual, disparador y alternativa de foco | +| Diálogos | [AssetEditorDialog](../modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx), [ResourceEditorDialog](../modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx): altura estable | +| Biblioteca propia | [FormDialog](../packages/rdx-ui/src/components/form-dialog.tsx): finalFocus; [FormStepActions](../packages/rdx-ui/src/components/form-step-actions.tsx): pie estable; [FormProgress](../packages/rdx-ui/src/components/form-progress.tsx): inicio del cuerpo al cambiar de paso | +| Listados e i18n | Seis pages de work-profiles, folder-mappings, application-configurations, certificate-configurations, printer-configurations y mail-configurations; common/locales/es.json y en.json de companies | +| Pruebas | visual-audit-ui.test.py, selection-components.test.tsx e inventory.test.tsx; profile.test.tsx, application.test.tsx, certificate.test.tsx, printer.test.tsx y mail.test.tsx | +| Documentación | Este seguimiento, DESIGN.md y componentes-de-interfaz.md | + +### Comprobaciones técnicas y limitaciones + +- **Build:** correcto; permanece el aviso de Vite sobre tamaño de chunks. +- **Tipos dirigidos:** correctos para rdx-ui, assets, resources y companies. +- **Typecheck global:** falla en vendor calendar.tsx:165 por locale y exactOptionalPropertyTypes; archivo sin modificar. +- **Lint global:** falla por errores en archivos ajenos al sprint, principalmente person-photo y procesos; también hay avisos en ejemplos de skills. Los archivos de código modificados pasan Biome dirigido. +- **Check global:** falla por los errores anteriores y formato preexistente, incluidos ejemplos de skills. No se reformatearon archivos ajenos para conseguir un resultado verde. +- **pnpm test:** ejecutado con MariaDB nueva desechable y variables explícitas, sin usar la base local ni la base del navegador. Pasan rdx-ui, assets y resources; companies mantiene dos fallos ajenos en companies-page.test.tsx: expectativas antiguas «Personas (1)» y «Esta persona no tiene acceso a Metis Desk». Son fuentes y pruebas no modificadas en este sprint. Turbo detiene el recorrido y no llega a ejecutar integración de servidor; no se declara superada la suite global. + +Durante la implementación se corrigieron una clave de traducción inicialmente no +existente y las expectativas anteriores de ocultar Anterior en rdx-ui y activos. También se descartaron +pasadas interrumpidas por HMR y errores de sintaxis/selectores del guion de inspección; +no se contabilizan como defectos de la aplicación ni como pruebas aprobadas. + +Al cerrar se identificó otro preparador anterior que no se había detectado al iniciar. +Se retiró exclusivamente la instancia creada para este sprint, su base y su sesión; +el entorno anterior permaneció activo. Las pruebas principales utilizaron la sesión +sintética del sprint. La comprobación final ampliada de contraste reutilizó la sesión +del preparador anterior, sin iniciar servicios ni escribir datos de negocio. Las +pruebas técnicas tuvieron bases separadas. +Se comprobó la eliminación de la sesión del sprint y de sus servicios. Las filas con +avisos de revisión no estaban pobladas en el navegador: la conservación de su motivo +se comprobó con renderizado SSR es/en. No se amplía la cobertura a modo oscuro u otros +navegadores por haber superado estos escenarios de Chromium. + +Se consultaron frontend-design, shadcn, vercel-composition-patterns y +vercel-react-best-practices: se conservaron identidad y tokens, se usó la API pública +Base UI para el foco y se mantuvieron composición y estado remoto existentes. No se +modificaron vendor, autenticación, modelo de datos, rutas ni los otros ocho hallazgos. diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index 9171c34..ea4690e 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -502,11 +502,14 @@ La edición y el alta anidada desde un puesto mantienen el formulario completo y recursos y activos: recibe paso/total, estado pendiente, disponibilidad de plantilla, callback de retroceso y etiquetas traducidas. Solo muestra Guardar al final (o en modo completo) y bloquea las acciones durante el envío; Cancelar lo aporta `FormDialog`. +En modo asistente mantiene Anterior visible y deshabilitado en el primer paso: así el +pie conserva el mismo número de filas al apilar acciones en móvil. En modo completo +no muestra Anterior. Ambos editores utilizan `layout="stable"` también en creación. No conoce entidades, formularios, consultas ni endpoints. ## 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, activos y 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 con `preventScroll`; al cambiar de paso sitúa el cuerpo al inicio para conservar visible el indicador, sin que el anuncio oculto lo desplace. Recibe etiquetas, descripción y textos accesibles del consumidor; no conoce entidades ni consultas. Lo usan los asistentes de recursos, activos y puestos. El diálogo de alta de puesto usa tamaño medio y cabecera con icono decorativo; la edición mantiene tamaño grande. Ambos utilizan `layout="stable"`. Asociaciones, activos, identificación y resumen utilizan @@ -1144,3 +1147,17 @@ Véase [integración visual de dominios](integracion-visual-de-dominios-windows. `RemoteOptionPicker` y `RemoteOptionList`, de `@repo/rdx-ui`, presentan una página aportada por el consumidor: búsqueda controlada, selección provisional, radios accesibles, columna de selección compacta y composición de la paginación. No aplican búsqueda, orden ni paginación locales. El diálogo mantiene altura estable y foco inicial en la búsqueda. Sus contratos no dependen de entidades, DTOs ni consultas de Metis. El consumidor gestiona carga/error, consulta remota, cursores opacos y textos traducidos. Assets los utiliza para seleccionar publicadores y carpetas SMB; Companies reutiliza el selector de activos para propietarios de entornos locales. Véase [integración visual de recursos compartidos SMB](integracion-visual-de-recursos-compartidos-smb.md). + +## Destino de foco al cerrar formularios + +`FormDialog.finalFocus` reexpone el contrato de `DialogContent`/Base UI: booleano, +referencia o callback que devuelve el destino. Si se omite, mantiene el comportamiento +de la primitiva. No implementa otro focus trap ni ejecuta un enfoque manual al cerrar. + +El inventario de activos captura el elemento enfocado en el evento de apertura, antes +de que el anuncio del asistente reciba foco, y lo proporciona mediante +`AssetEditorDialog.finalFocus`. Si continúa conectado y habilitado, Base UI vuelve a +él. Si desaparece o se deshabilita, se ofrece Nuevo activo si está disponible, la +búsqueda del inventario o su contenedor como último destino local. El contenedor no se +añade a la secuencia Tab. El selector anidado de puestos conserva su foco inicial y +su retorno al formulario padre. diff --git a/docs/propuesta-navegacion-responsive-v01.md b/docs/propuesta-navegacion-responsive-v01.md new file mode 100644 index 0000000..86abaf3 --- /dev/null +++ b/docs/propuesta-navegacion-responsive-v01.md @@ -0,0 +1,476 @@ +# V01 — Propuesta de navegación y layout responsive + +Fecha: 21 de octubre de 2026. **Propuesta para aprobación; sin implementar. V01 continúa pendiente.** + +## 1. Resumen ejecutivo + +Recomiendo conservar los dos ámbitos de navegación, pero cambiar su presentación según la capacidad disponible: barra global expandida en escritorio amplio, compacta en portátil y superpuesta en ventanas estrechas; navegación de compañía permanente cuando quepa junto al contenido y superpuesta cuando reduzca su capacidad operativa. Una franja contextual mantiene visibles la compañía y la sección, incluso con ambas navegaciones cerradas. + +El inventario decide entre tabla con panel y tabla con detalle modal por **ancho de su propio contenedor**, después de descontar navegaciones y rellenos. Contraer únicamente el menú global mejora el problema, pero no lo resuelve: a 1100 px la tabla pasa de 237 a 393 px con panel abierto, todavía insuficiente para comparar activos. + +La intervención conserva React, Tailwind, Base UI, shadcn/ui, `@repo/rdx-ui`, iconografía, datos y rutas. No propone una barra horizontal de 21 destinos ni una nueva biblioteca. La arquitectura elegida combina B —superposición contextual— con el colapso global de A y una franja de orientación de C. No se necesita una refactorización general del shell. + +Las cifras de propuesta son presupuestos de diseño pendientes de validación en una implementación. Las mediciones de la sección 4 pertenecen a la aplicación actual y están separadas de esas estimaciones. En esta tarea solo se crea este documento; no cambia DESIGN.md ni el estado de otros hallazgos. + +## 2. Problema y evidencias + +Se reutilizó el entorno sintético existente de `scripts/visual-audit.py`, con sesión privada de usuario ordinario en `authenticated-vmpuj3f2`. No se prepararon compañías, usuarios, bases ni servicios nuevos. El cliente MCP directo abrió inicialmente el acceso sin autenticar; para la matriz se utilizó el cliente stdio del preparador, con su archivo de sesión montado en modo lectura en un Chromium efímero. No se imprimieron cookies ni credenciales. Se bloquearon las peticiones HTTP de escritura desde el navegador. + +Se navegaron seis escenarios en ocho resoluciones: **48 geometrías y 48 capturas**, además de tres capturas y comprobaciones específicas de colapso/desplazamiento. Se esperó al workspace, a la desaparición de la carga visible, a las fuentes y a las animaciones finitas; se dejaron transcurrir dos frames antes de medir. Se abrieron explícitamente las capturas citadas de móvil, tablet y panel en escritorio. No se atribuyen resultados a un prototipo: las imágenes muestran exclusivamente la interfaz vigente. + +Resultados principales: + +- Las medidas antiguas de compañía se reproducen: inicio de `main` en Y=1429,66 a 390 px y Y=1496,31 a 320 px. La navegación contextual tiene 21 destinos y ocupa 1014 px de alto en ambos móviles. +- A 768 px, `main` mide 300 px exteriores; descontando borde/rellenos quedan aproximadamente 251 px. El viewport interior de tabla mide 249 px. Esta diferencia de 2 px explica las distintas cifras sin contradecir la auditoría. +- A 1100 px, tabla/panel miden 237/320 px. La tabla tiene 808 px de contenido desplazable: el viewport representa aproximadamente el 29 % de ese ancho. +- A 1440 px la tabla con panel mide ahora 551 px, frente a 495 px a 1366. No sustituir todas las cifras por la referencia anterior de 1366. +- Dashboard y directorio también acumulan navegación global en móvil: comienzan en Y=296 y Y=344 respectivamente. +- No se detectó scroll horizontal del documento en los 48 casos. Sí existe desplazamiento horizontal interno de tabla: es deliberado y debe conservarse. +- Tras desplazar el contenido de Activos a la vista en móvil, búsqueda global y sección activa quedan por encima del viewport. El problema afecta a la orientación además del espacio. + +Evidencias: [Activos a 390](../.artifacts/playwright/v01-design/assets-390.png), [Activos a 768](../.artifacts/playwright/v01-design/assets-768.png), [panel a 1440](../.artifacts/playwright/v01-design/asset-panel-1440.png), [panel a 1100](../.artifacts/playwright/v01-design/asset-panel-1100.png), [global contraída a 1100](../.artifacts/playwright/v01-design/collapsed-panel-1100.png), [contenido a 320 tras scroll](../.artifacts/playwright/v01-design/assets-scrolled-320.png). + +## 3. Arquitectura actual + +### Composición, dimensiones y desplazamiento + +```text +CompaniesWorkspace — flex columna; desde shell, flex fila y h-dvh +├─ WorkspaceSidebar — 228 px / 72 px; móvil: bloque w-full anterior al contenido +│ ├─ marca +│ ├─ nav global + compañías, scroll propio en escritorio +│ └─ configuración y colapso, pie fuera del scroll del nav +└─ contenedor flex-1 min-w-0 overflow-y-auto + ├─ WorkspaceHeader — 88 px escritorio; 74 px medidos en móvil + └─ CompanyDetail embedded — grid + ├─ identidad compañía — columna izquierda / bloque superior móvil + ├─ nav compañía — 240 px / bloque superior móvil + └─ main — minmax(0,1fr), p-6 / p-4 + └─ inventario: listado + gap 24 + panel clamp(320px,24vw,400px) +``` + +`shell=768px`, `inventory=1100px`, `dashboard=1200px` son media queries de viewport. El `@container` de fichas no gobierna el reparto tabla/panel. `useCompactDetail` consulta `(max-width:1099px)`: CSS y React duplican el umbral y conocen el viewport, no la capacidad restante. + +La secundaria usa un grid fijo de 240 px. En móvil cambia de área, pero cada enlace sigue a ancho completo: envolver el `nav` con flex no compacta sus 21 destinos. En escritorio la secundaria comparte el desplazamiento del contenido; no tiene scroll independiente. El panel no es sticky ni tiene altura propia limitada: el ejemplo de 1100 px ocupa unos 3853 px de alto, con sus secciones abiertas. La tabla mantiene las acciones ancladas a la derecha mediante `position:sticky`; la cabecera de tabla solo es sticky en la variante `layout="fill"`, no universalmente. Cabecera global e identidad no son sticky actualmente. + +### Navegación, DOM, estado y foco + +El orden DOM es global → cabecera → identidad → navegación contextual → `main`; el teclado atraviesa enlaces anteriores al contenido. `WorkspaceNavigationButton` compone `BrowserLink` para destinos, con `aria-current="page"` y estado visual derivado del ámbito. `CompanyDetail` usa enlaces cuando recibe navegación por rutas y botones en el modo local. Mantener esa distinción, sin introducir menús de aplicación ARIA para una lista normal de enlaces. + +La barra global persiste su booleano con `metis-desk:sidebar-collapsed` en localStorage del origen, compartido entre cuentas de ese navegador; se confirmó su recuperación al recargar. No hay preferencia equivalente para la compañía. La URL controla compañía, sección y fichas; búsqueda y plantilla están conectadas a la query. `main key={tab}` remonta al cambiar de sección. No existe aquí un contrato explícito general de foco al nuevo título ni un enlace de salto al contenido. + +En activos, seleccionar la zona no interactiva de una fila mantiene la selección contextual local; el nombre enlaza a la ficha completa. Una URL directa de activo abre `AssetDetailPage`. En recursos hay diferencias en la relación entre selección y ruta. **No unificar estas semánticas como efecto lateral del layout.** Hay que probar cada controlador. El detalle compacto existente usa diálogos; Base UI resuelve modalidad y foco. El cambio de composición necesita coordinar ese contrato, no simplemente ocultar un `aside` con CSS. + +## 4. Matriz de geometrías observadas + +Todas las dimensiones siguientes son píxeles CSS, redondeadas a la unidad. Global expandida, una compañía sintética, columnas iniciales. `main` incluye rellenos; tabla útil es `clientWidth` de su contenedor horizontal. «—» indica que el elemento no existe o no ocupa ancho lateral, no una prueba pendiente. + +### Dashboard + +| Viewport | Global | Nav. compañía | main | Tabla útil | Panel / diálogo | H previa | Búsqueda / compañía / sección visibles | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1920×1080 | 228 lateral | — | 1692 | 768 | — | 88 | sí / — / — | +| 1440×900 | 228 lateral | — | 1212 | 528 | — | 88 | sí / — / — | +| 1366×768 | 228 lateral | — | 1138 | 491 | — | 88 | sí / — / — | +| 1100×800 | 228 lateral | — | 872 | 782 | — | 88 | sí / — / — | +| 1024×768 | 228 lateral | — | 796 | 706 | — | 88 | sí / — / — | +| 768×1024 | 228 lateral | — | 540 | 450 | — | 88 | sí / — / — | +| 390×844 | 390 apilada | — | 390 | 324 | — | 296 | sí / — / — | +| 320×640 | 320 apilada | — | 320 | 254 | — | 344 | sí / — / — | + +### Compañías + +| Viewport | Global | Nav. compañía | main | Tabla útil | Panel / diálogo | H previa | Búsqueda / compañía / sección visibles | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1920×1080 | 228 lateral | — | 1692 | 1642 | — | 88 | sí / — / — | +| 1440×900 | 228 lateral | — | 1212 | 1162 | — | 88 | sí / — / — | +| 1366×768 | 228 lateral | — | 1138 | 1088 | — | 88 | sí / — / — | +| 1100×800 | 228 lateral | — | 872 | 822 | — | 88 | sí / — / — | +| 1024×768 | 228 lateral | — | 796 | 746 | — | 88 | sí / — / — | +| 768×1024 | 228 lateral | — | 540 | 490 | — | 88 | sí / — / — | +| 390×844 | 390 apilada | — | 390 | 356 | — | 296 | sí / — / — | +| 320×640 | 320 apilada | — | 320 | 286 | — | 344 | sí / — / — | + +### Resumen de compañía + +| Viewport | Global | Nav. compañía | main | Tabla útil | Panel / diálogo | H previa | Búsqueda / compañía / sección visibles | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1920×1080 | 228 lateral | 240 | 1452 | — | — | 88 | sí / sí / sí | +| 1440×900 | 228 lateral | 240 | 972 | — | — | 88 | sí / sí / sí | +| 1366×768 | 228 lateral | 240 | 898 | — | — | 88 | sí / sí / sí | +| 1100×800 | 228 lateral | 240 | 632 | — | — | 88 | sí / sí / sí | +| 1024×768 | 228 lateral | 240 | 556 | — | — | 88 | sí / sí / sí | +| 768×1024 | 228 lateral | 240 | 300 | — | — | 88 | sí / sí / sí | +| 390×844 | 390 apilada | 390 apilada | 390 | — | — | 1430 | sí / sí / sí | +| 320×640 | 320 apilada | 320 apilada | 320 | — | — | 1496 | sí / sí / sí | + +### Inventario de activos + +| Viewport | Global | Nav. compañía | main | Tabla útil | Panel / diálogo | H previa | Búsqueda / compañía / sección visibles | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1920×1080 | 228 lateral | 240 | 1452 | 1401 | — | 88 | sí / sí / sí | +| 1440×900 | 228 lateral | 240 | 972 | 921 | — | 88 | sí / sí / sí | +| 1366×768 | 228 lateral | 240 | 898 | 847 | — | 88 | sí / sí / sí | +| 1100×800 | 228 lateral | 240 | 632 | 581 | — | 88 | sí / sí / sí | +| 1024×768 | 228 lateral | 240 | 556 | 505 | — | 88 | sí / sí / sí | +| 768×1024 | 228 lateral | 240 | 300 | 249 | — | 88 | sí / sí / sí | +| 390×844 | 390 apilada | 390 apilada | 390 | 356 | — | 1430 | sí / sí / sí | +| 320×640 | 320 apilada | 320 apilada | 320 | 286 | — | 1496 | sí / sí / no | + +### Ficha completa de activo + +| Viewport | Global | Nav. compañía | main | Tabla útil | Panel / diálogo | H previa | Búsqueda / compañía / sección visibles | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1920×1080 | 228 lateral | 240 | 1452 | 1353 | — | 88 | sí / sí / sí | +| 1440×900 | 228 lateral | 240 | 972 | 873 | — | 88 | sí / sí / sí | +| 1366×768 | 228 lateral | 240 | 898 | 799 | — | 88 | sí / sí / sí | +| 1100×800 | 228 lateral | 240 | 632 | 533 | — | 88 | sí / sí / sí | +| 1024×768 | 228 lateral | 240 | 556 | 457 | — | 88 | sí / sí / sí | +| 768×1024 | 228 lateral | 240 | 300 | 201 | — | 88 | sí / sí / sí | +| 390×844 | 390 apilada | 390 apilada | 390 | 308 | — | 1430 | sí / sí / sí | +| 320×640 | 320 apilada | 320 apilada | 320 | 238 | — | 1496 | sí / sí / no | + +### Inventario con consulta contextual + +| Viewport | Global | Nav. compañía | main | Tabla útil | Panel / diálogo | H previa | Búsqueda / compañía / sección visibles | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1920×1080 | 228 lateral | 240 | 1452 | 977 | 400 panel | 88 | sí / sí / sí | +| 1440×900 | 228 lateral | 240 | 972 | 551 | 346 panel | 88 | sí / sí / sí | +| 1366×768 | 228 lateral | 240 | 898 | 495 | 328 panel | 88 | sí / sí / sí | +| 1100×800 | 228 lateral | 240 | 632 | 237 | 320 panel | 88 | sí / sí / sí | +| 1024×768 | 228 lateral | 240 | 556 | 505 | 922 diálogo | 88 | sí / sí / sí | +| 768×1024 | 228 lateral | 240 | 300 | 249 | 691 diálogo | 88 | sí / sí / sí | +| 390×844 | 390 apilada | 390 apilada | 390 | 356 | 358 diálogo | 1430 | sí / sí / sí | +| 320×640 | 320 apilada | 320 apilada | 320 | 286 | 288 diálogo | 1496 | sí / sí / no | + +En la columna «H previa», el escenario contextual utiliza la posición inicial del inventario antes de pulsar la fila. Las coordenadas posteriores al clic y al desplazamiento automático se conservan separadas en el JSON. El ancho de navegación apilada es el del bloque superior, no una reserva lateral. En dashboard/directorio no hay compañía ni sección contextual. + +### Altura y acceso al contenido + +En desktop el encabezado precede al contenido con 88 px; la navegación queda al lado. En móvil el bloque global ocupa 222/270 px, seguido de cabecera de 74 px. En compañía se añaden identidad y 1014 px de navegación contextual. Las Y anteriores son la **altura previa acumulada hasta `main`**, no una medición aislada del `nav`. + +Desde una URL ya cargada, dashboard/directorio requieren cero activaciones para ver su título en las ocho resoluciones; compañía también en seis tamaños desde 768 px. En 390/320 se necesita desplazamiento para alcanzar `main`, sin activación previa de un menú. Se ejecutó una operación `scrollIntoViewIfNeeded` por tamaño y el contenido quedó visible; esto **no significa un único gesto físico** ni mide tiempo de usuario. Abrir el panel/detalle contextual requirió una pulsación en una celda no interactiva de la fila, en los ocho tamaños. Acceso por teclado completo y coste en tabulaciones no se midieron en esta matriz. + +Al cargar compañía: búsqueda global e identidad visibles en las ocho resoluciones; sección activa visible en siete, fuera de la primera pantalla en 320 px. Al desplazar Activos: en 390 la búsqueda queda en Y=−921 y la sección activa en Y≈−515; en 320, Y=−1223 y Y≈−799. Son posiciones asentadas tras desplazamiento, no geometrías transitorias. La matriz de panel registra también el desplazamiento provocado al pulsar la fila: sus Y no deben reemplazar las medidas iniciales sin panel. + +La consola de la matriz autenticada terminó con **0 errores y 0 avisos**. El acceso inicial del cliente directo sin sesión produjo respuestas de autenticación; no se contabilizan como fallo visual de la matriz autenticada. + +## 5. Alternativas estudiadas + +### A — Dos navegaciones colapsables + +Conservar global 228/72 y añadir modo compacto de compañía. Ventaja: destinos frecuentes a una pulsación y transición próxima al modelo actual. Inconveniente: 21 iconos contextuales, varios semánticamente próximos, dependen demasiado de reconocimiento y ayudas. Dos raíles siguen reservando ancho en tablet. En móvil no deben volver a apilarse todos los destinos: esta alternativa necesitaría además una superposición. + +Persistir intención de expansión, mantener nombre accesible y tooltip al hover/foco. El tooltip ayuda a interpretar, pero no sustituye la etiqueta disponible en una navegación abierta. No persistir colapsos automáticos producidos por una ventana pequeña. El panel aún requiere regla de capacidad propia. **Descartada como solución completa**; se conserva su raíl global, ya implementado y con pocos destinos principales. + +### B — Navegación contextual superpuesta + +Global permanente cuando quepa; compañía como `Sheet` lateral modal cuando su columna perjudique al contenido. Un botón «Secciones de [compañía]» junto al destino actual abre todos los enlaces en una lista etiquetada y desplazable. Identidad y sección se mantienen fuera de la superposición. Apertura/cierre, bloqueo de fondo y foco se delegan a Base UI. + +Recupera los 240 px de la secundaria cuando se oculta. Añade una activación para cambiar de sección si estaba cerrada, a cambio de mostrar etiquetas y jerarquía completa. En escritorio amplio mantiene navegación permanente. Es coherente con 21 destinos y con el detalle modal existente. **Es la base recomendada.** + +### C — Navegación contextual integrada en cabecera + +Sustituir la secundaria por un selector de sección próximo al título y a un selector de compañía. Recupera 240 px sin una columna permanente, incluso en escritorio amplio. Una barra horizontal con 21 enlaces requiere overflow, oculta destinos y confunde navegación contextual con las pestañas de plantilla del inventario. No recomendar esa barra. + +Un botón con popover de enlaces es viable para ventanas amplias y pocas opciones; para el conjunto actual necesita scroll, grupos y adaptación a modal. Un `Select` de formulario no conserva naturalmente abrir en nueva pestaña ni clic modificado. **Descartada como arquitectura principal** por descubribilidad y navegación frecuente; se aprovecha su franja contextual de compañía/sección. No se añade un selector de compañía que duplique estados: cambiar compañía abre el selector global existente o sus enlaces. + +## 6. Comparación de alternativas + +Juicios de diseño, sin pruebas de usabilidad de alternativas y sin puntuación total. Recuperación de ancho calculada a partir de dimensiones actuales; complejidad y riesgos son valoraciones cualitativas. + +| Criterio | A: dos colapsables | B: contextual superpuesta | C: contextual integrada | +| --- | --- | --- | --- | +| Espacio recuperado | Global: 156 px; secundaria: depende del raíl nuevo | 240 px contextuales; global puede recuperar 156 o 228 px | 240 px contextuales; añade banda superior | +| Jerarquía global/compañía | Clara expandida; dos raíles la debilitan | Clara con identidad permanente y dos activadores diferenciados | Clara si no se mezcla selector de compañía y sección | +| Cambio de sección | Una activación si visible | Una con secundaria fija; dos desde menú cerrado | Dos desde selector cerrado | +| Orientación | Etiquetas ocultas en compacto | Compañía y sección siempre visibles | Buena con franja persistente | +| Teclado | Muchos enlaces antes del contenido; requiere salto | Lista de enlaces en modal, retorno y salto | Popover/lista viable; Select limita enlaces | +| Móvil | Precisa mecanismo adicional | Resuelve navegación sin preceder al contenido | Selector viable; barra extensa inadecuada | +| Tablet | Dos raíles siguen restando | Cede ancho a la tarea | Cede ancho; apertura frecuente | +| Escritorio amplio | Familiar | Mantiene secundaria fija | Pierde exploración simultánea de destinos | +| Panel contextual | No resuelto por sí solo | Admite capacidad independiente | Admite capacidad independiente | +| Implementación | Media: segunda navegación compacta y adaptación móvil | Media: Sheet y una política responsive | Media: reubicar todos los destinos y adaptación popup/modal | +| Reutilización | Alta global; nueva secundaria de iconos | Alta: enlaces actuales, Sheet y diálogos | Media/alta: enlaces actuales; nuevo selector de destinos | +| Regresiones | Etiquetas, tooltips, anchos y estados de dos raíles | Foco, resize y scroll de overlay | Historial/clic modificado si se usa un Select | +| Mantenimiento | Dos colapsos y fallback móvil | Un modelo de destinos con presentaciones distintas | Manejo de overflow/subniveles y presentaciones | +| DESIGN.md | Conserva identidad, requiere revisar §4/10 | Conserva identidad, requiere aprobar cambio móvil §4/10 | Conserva identidad; revisar patrón de navegación y pestañas | + +Elegir B híbrida porque preserva navegación directa donde hay capacidad y evita transformar la navegación de compañía en un vocabulario de iconos. El coste adicional de apertura queda explícito; no se afirma que produzca tiempos de tarea inferiores sin probarlo. + +## 7. Arquitectura recomendada + +### Modelo y presupuestos propuestos + +Separar **preferencia** de **modo efectivo**. Preferencia global actual expandida/contraída; secundaria inicialmente automática, con acción explícita «Ocultar navegación» cuando está fija y «Fijar navegación» cuando haya capacidad. Persistir solo esa intención si se aprueba, bajo clave propia versionada; no almacenar compañía, sesión, filtros o selección en esa clave. Los overlays siempre arrancan cerrados. La adaptación automática nunca sobrescribe la preferencia guardada. + +Presupuestos iniciales para validar: lista operativa de referencia **800 px**, secundaria 240, global expandida 228/compacta 72, rellenos desktop 48, gap tabla/panel 24, panel 320–400. Los valores existentes se conservan; 800 es una **hipótesis de diseño**, fundamentada en los 808 px de tabla mínima observada con columnas iniciales. Permite un pequeño scroll interno, pero evita reservar solo 237 px para comparar. No se exige ese mínimo a una pantalla cuyo ancho físico sea inferior; allí la tabla recibe toda la anchura disponible. + +Reglas iniciales, expresadas en capacidad del shell y no en nombres de dispositivo: + +1. Si caben 72 + 48 + 800 = **920 px**, global compacta disponible; por debajo, global superpuesta. No añadir un raíl de 72 px a un móvil de 320. +2. Fijar secundaria solo si el espacio posterior a global admite 240 + 48 + 800 = **1088 px**. Con global compacta, el umbral nominal equivale a 1160 px de shell. +3. Global expandida automática en ancho suficiente para secundaria y tabla/panel mínimo: 228 + 240 + 48 + 800 + 24 + 320 = **1660 px**. La preferencia contraída continúa respetándose por encima. Estas sumas son un criterio de partida, no un breakpoint vigente. +4. Por debajo de esos presupuestos, la expansión global solicitada se presenta temporalmente como overlay sin reducir el contenido. Fijar la secundaria se deshabilita si no cabe y explica el motivo. No permitir estados permanentes que vulneren el presupuesto para ganar una preferencia visual. +5. Evaluar panel **después** del modo de navegación; abrirlo no provoca automáticamente colapsos del shell. Así se evita una reacción circular navegación ↔ panel. + +### Presentación por capacidad + +| Configuración representativa | Global | Compañía | Contenido y panel | +| --- | --- | --- | --- | +| 1920, escritorio amplio | Expandida o 72 según preferencia | 240 fija, ocultable | Tabla y panel si cumplen su regla; con valores actuales sí | +| 1440/1366, portátil | Raíl 72; expansión en overlay | 240 fija, ocultable | Tabla completa; detalle modal por defecto con secundaria fija | +| 1100/1024, portátil estrecho/tablet | Raíl 72 | Overlay | Prioridad a tabla; detalle modal si no cabe al lado | +| 768, tablet | Overlay | Overlay | Toda el área útil; sin dos menús superpuestos simultáneamente | +| 390/320, móvil | Overlay | Overlay | Contenido inmediato; tabla con scroll interno y detalle modal | + +La tabla es la aplicación estimada de las reglas, no una implementación medida. Dashboard/directorio no reservan secundaria de compañía. Las fichas completas ocupan el ancho de contenido y no abren un panel paralelo adicional. + +### Cabecera, contexto y dimensiones verticales + +Conservar cabecera desktop de 88 px. Proponer 56 px para cabecera compacta: menú global, marca compacta, acceso a búsqueda y cuenta; la campana sin servicio no debe desplazar controles necesarios, pero cambiar su función queda fuera de V01. Por debajo de la capacidad de un campo de búsqueda, usar botón «Buscar en Metis Desk» que abre la misma búsqueda limitada a compañías/destinos; no promete búsqueda transversal de activos. En anchos suficientes, mantener el input. + +Añadir franja contextual nominal de 48 px en páginas de compañía: identidad con avatar pequeño, nombre accesible completo, sección y botón «Secciones». En móvil de 320 px puede ocupar dos filas hasta 72 px para que no desaparezca la sección. Nombre largo truncado visualmente con acceso al texto completo; no truncar el nombre accesible. El título de listado mantiene su receta y CTA. Editar compañía pasa a una acción contextual explícita; no eliminarla al retirar la gran identidad lateral móvil. + +Cabecera y franja contextual sticky dentro de **un único scroll principal**; en desktop, la secundaria tiene scroll propio en el alto restante. No introducir un segundo scroll vertical en el listado normal. El scroll horizontal permanece dentro de la tabla. Ajustar `scroll-padding-top`/`scroll-margin-top` para no tapar foco, anclas o título con el bloque sticky. Con zoom/ventana de poca altura, permitir que la búsqueda abierta se desplace internamente y no añadir filas permanentes de controles. + +### Resize y recuperación + +Cambiar modo no cambia URL, filtros, orden, página, selección ni entidad abierta. Mantener montada la tabla y sus nodos de scroll. El detalle puede cambiar de presentación conservando ID y lectura; no mantener a la vez una copia de panel y otra de diálogo con controles activos. No persistir coordenadas o anchos observados. + +Si un overlay pasa a navegación fija al ensanchar: cerrar modalidad, liberar scroll/inert y llevar foco al enlace equivalente si estaba dentro. Si continúa superpuesto, conservar apertura. Al estrechar una navegación fija, no abrir un modal sin acción del usuario: si el foco queda en un enlace que desaparece, moverlo a «Secciones» o «Menú global». La selección no cambia. Si el foco estaba en tabla, no moverlo. Una única superposición de navegación puede estar abierta: abrir la otra cierra la primera con transferencia de foco al nuevo destino, sin salto intermedio al fondo. + +## 8. Wireframes por tamaño + +Esquemas de diseño, **no capturas ni componentes implementados**. Las cajas representan responsabilidades, no nuevas tarjetas. `G` = global; `C` = compañía. + +### Escritorio amplio, navegación y panel + +```text +┌─ G 228 ──────┬──── cabecera 88: búsqueda / cuenta ────────────────────┐ +│ Metis Desk ├──── compañía · Activos · Secciones / editar compañía ──┤ +│ Inicio │ C 240 │ Activos [Nuevo activo] │ +│ Compañías │ Resumen │ pestañas / buscar │ +│ Catálogos │ Personas │ ┌── tabla ≥800 ─────┐ ┌─ panel 320–400 ┐│ +│ Compañía │ Activos ● │ │ nombre … acciones │ │ activo [Cerrar]││ +│ … │ Recursos │ │ filas │ │ resumen/datos ││ +│ Ajustes │ … scroll │ └─ paginación ──────┘ └───────────────┘│ +└──────────────┴─────────────┴─────────────────────────────────────────┘ +``` + +### Portátil, inventario + +```text +┌ G72 ┬── cabecera: búsqueda / cuenta ────────────────────────────────┐ +│logo ├── compañía · Activos · [Secciones] ──────────────────────────┤ +│icon │ C240, ocultable │ Activos [Nuevo activo] │ +│icon │ destinos │ buscar / filtros / columnas │ +│icon │ Activos ● │ tabla: todo el espacio disponible │ +│ … │ … │ paginación │ +│ │ │ seleccionar → diálogo contextual │ +└─────┴────────────────┴─────────────────────────────────────────────┘ +``` + +Ocultar C puede habilitar panel si cumple capacidad; no se abre solo por ocultarla. La consulta seleccionada conserva su estado abierto/cerrado. + +### Tablet, navegación contextual abierta + +```text +┌ [Menú global] Metis / Buscar / cuenta ──────────────────────────┐ +├ compañía · Activos [Secciones] ────────────┤ +│ ┌─ Sheet C, título y [Cerrar] ──┐ fondo inerte │ +│ │ compañía │ Activos [Nuevo activo] │ +│ │ Resumen │ buscar / tabla │ +│ │ Activos ● │ paginación │ +│ │ todos los destinos, scroll │ selección → detalle modal │ +│ └──────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────┘ +``` + +A 1024 puede haber raíl global; a 768 no se reserva. El botón dibujado es el caso de 768. El detalle modal y el menú no se abren simultáneamente por una misma acción. + +### Móvil, navegación cerrada + +```text +┌ [Menú] Metis [Buscar] [Cuenta] ┐ +│ compañía sintética [Secciones] │ +│ Activos │ +├ Activos [Nuevo activo] ┤ +│ pestañas / búsqueda │ +│ columnas / orden │ +│ tabla ↔ scroll propio, acciones │ +│ paginación │ +└──────────────────────────────────────┘ +``` + +Seleccionar fila abre detalle modal; enlace de nombre conserva la ficha completa. Las acciones principales siguen en el contenido, no ocultas en navegación. + +### Móvil, navegación contextual abierta + +```text +┌ Secciones de compañía [Cerrar]┐ +│ compañía sintética │ +│ [Cambiar compañía] │ +│ Resumen │ +│ Personas (0) │ +│ Activos (1) ← actual │ +│ Recursos (0) │ +│ … todos los destinos, scroll interno │ +│ [Editar compañía] │ +└──────────────────────────────────────┘ + fondo modal bloqueado; global cerrada +``` + +Anchura del Sheet limitada al viewport; en 320 px puede ocuparlo completo. Botón de cierre visible y títulos etiquetados. Cambiar compañía cierra el contextual y abre navegación global en su bloque de compañías; no apila dos modales. + +## 9. Contratos de interacción + +| Acción/situación | Contrato propuesto | +| --- | --- | +| Tab / Shift+Tab | Orden DOM lógico. Primer acceso «Saltar al contenido»; después cabecera/contexto, navegación fija y contenido. En modal, recorrido contenido en la primitiva Base UI; no alcanzar el fondo. | +| Enter | Activa enlaces de destino y botones. Clic modificado/medio conserva comportamiento de enlace. | +| Espacio | Activa botones de apertura/cierre; en enlaces mantiene comportamiento nativo de página, sin emular un botón. | +| Abrir navegación | Botón con nombre, `aria-expanded`, relación con contenido; Sheet con título. Foco en enlace activo; si no existe, primer destino. No introducir búsqueda de secciones sin necesidad. | +| Escape / Cerrar / fondo | Cierra la navegación sin cambiar ruta y devuelve foco al disparador vigente. Si desapareció por resize, destino equivalente visible en cabecera; no al body. | +| Elegir destino ordinario | Navega mediante BrowserLink, cierra overlay y enfoca título principal de destino con `tabIndex=-1`; no devolver foco al disparador al mismo tiempo. Una única política de foco, apoyada en finalFocus de Base UI. | +| Elegir destino con Ctrl/Cmd/Shift/medio | Mantiene semántica del enlace; no cerrar ni mover el foco del documento original si no navega en él. Respetar target y download. | +| Sección activa | `aria-current="page"`, estilo y texto. Lista contextual con nombre «Secciones de [compañía]» y global con nombre distinto. No marcar dos destinos del mismo ámbito como actuales. | +| Búsqueda global | Conservar ámbito actual y Escape. En compacto se abre mediante botón; foco en búsqueda y retorno al botón. Cerrar navegación antes de abrir búsqueda, sin cambiar ruta. V07/búsqueda de tildes queda fuera. | +| URL directa / recarga | Derivar identidad y sección de las rutas actuales; overlays cerrados. No derivarlas del último elemento pulsado ni de localStorage. | +| Atrás / Adelante | Conservar contratos de History API y filtros; apertura/cierre de menús no crea entradas. Cerrar una ficha conserva su retorno interno actual. | +| Cambio de compañía | Usar destino existente de resumen, actualizar contexto y cerrar overlay; no arrastrar filtros específicos o IDs de la compañía anterior. No diseñar aquí restauración nueva por compañía. | +| Panel lateral | Complementary con nombre y acción de cierre. Foco no atrapado; acción explícita «Ir al detalle» si se necesita acceso directo por teclado. Selección de fila no roba foco al ratón. | +| Detalle compacto | Dialog con título, foco inicial pertinente y retorno a fila/acción; fallback a búsqueda local. Escape cierra consulta, mantiene selección contextual cuando el consumidor lo permita. | +| Resize con detalle | Si foco estaba dentro del detalle que migra, trasladarlo a su equivalente; si estaba en tabla, no abrir modalidad por el mero resize: contraer la consulta y mantener selección con «Ver detalle». No remontar tabla. | +| Scroll / zoom | Fondo bloqueado solo durante modalidad; panel fijo no lo bloquea. Scroll interno de navegación larga; sin scroll horizontal del documento. A 200/400 %, composición según ancho CSS efectivo, foco no oculto bajo sticky. | + +Reutilizar el `Sheet` instalado, basado en `@base-ui/react/dialog`, y sus APIs públicas. No escribir focus trap, listeners Escape globales o gestión manual de `inert`. El `SheetContent` vendor permite composición y props de Popup; el cierre traducido se aporta desde el consumidor si el cierre vendor tiene literal inglés. No editar vendor. La [documentación de Dialog de Base UI](https://base-ui.com/react/components/dialog) respalda delegar modalidad y foco; las APIs concretas deberán contrastarse con los tipos instalados durante la implementación. + +## 10. Reglas para tabla y panel contextual + +### Capacidad y mínimo operativo + +Medir el **content box estable del contenedor exterior del inventario**, antes de repartir las columnas. Sea `W` ese ancho, `T=800`, `G=24` y `P` el panel propuesto. Mostrar panel lateral solo si `W ≥ T + G + P`. El panel puede crecer de 320 a 400 usando el espacio sobrante, conservando `W − G − P ≥ T`. Umbral inicial mínimo: **1144 px de inventario**. Bordes interiores de tabla reducen aproximadamente 2 px el viewport: las pruebas deben distinguir ancho de columna/listado de `clientWidth` de tabla. + +La referencia de 800 procede del ancho de 808 observado con icono, nombre, plantilla, estado, sede y acciones. No garantiza que todas las columnas opcionales quepan; siguen desplazándose internamente. Validar con nombres largos, texto en inglés, 10/50 filas y combinaciones de columnas antes de fijar el umbral. No usar el ancho total dinámico de todas las columnas como condición: elegir más campos no debe provocar oscilación de presentación. Este mínimo es específico del inventario de referencia; otro listado deberá justificar su presupuesto. + +### CSS, ResizeObserver y fuente de verdad + +- **Solo container queries:** suficientes para cambiar distribución, separaciones y controles visuales; insuficientes para decidir qué árbol semántico modal montar, transferir foco y conservar lectura. +- **Solo ResizeObserver:** puede decidir todo, pero trasladar todos los detalles visuales a React añade trabajo innecesario. +- **Combinación recomendada:** un único hook de capacidad del inventario con un `ResizeObserver` sobre el contenedor exterior estable; publica un modo discreto `inline`/`overlay`. React monta una presentación de detalle. El grid y `SelectionPanel` consumen ese modo mediante atributo/variante explícita, sin volver a decidirlo con `inventory:` o un segundo umbral. Container queries se reservan al responsive interior de tabla, franja y detalle. [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) informa del tamaño del elemento; las [container queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries) adaptan estilos de descendientes. No sustituir esto por medir `window.innerWidth`. + +Leer anchura sin publicar cada píxel: notificar solo cambios de modo, limpiar el observer al desmontar y comenzar en modo seguro compacto hasta tener medida. No observar la columna de tabla que encoge al abrir panel. Con contenedor estable, no hace falta histéresis de partida; si se reproduce oscilación por scrollbar, corregir geometría/gutter antes de añadir umbrales diferentes. Un solo origen para constantes de capacidad; evitar duplicarlas en CSS y hook. + +La política del shell se evalúa sobre su contenedor raíz, independiente de la apertura del detalle. Puede apoyarse en una suscripción discreta a sus límites estables; si la implementación usa medición de elemento, una sola medición por frontera real, nunca un observer por enlace. No crear un gestor responsive universal ni un contexto global de cada píxel. + +### Estimación de ganancia, no medición de solución + +Con rellenos actuales y política propuesta: 1920 deja aproximadamente 1403 px interiores con ambas navegaciones; panel de 400 y gap 24 dejan unos 979 para listado, equivalente al escritorio amplio actual. A 1440/1366 con global de 72 y secundaria de 240 quedan unos 1079/1005 interiores; el detalle se abre en diálogo y el listado dispone de todo ese ancho. A 1100/1024 con global compacta y contextual superpuesta quedan unos 979/903; también diálogo. A 768 con ambas superpuestas y p-4 quedan unos 736; a 390/320, 358/288. Descontar bordes de tabla al comparar con las mediciones. + +Ocultar manualmente secundaria en 1440 deja aproximadamente 1319 px interiores: puede habilitar tabla/panel conservando el mínimo. No cerrar/abrir panel automáticamente por cambiar columnas. Preservar búsqueda, plantilla, orden local, página, fila seleccionada y scroll horizontal/vertical durante cambios de modo, manteniendo estable la instancia de tabla. La restauración tras navegar a otra página, V05, sigue siendo otra tarea. + +## 11. Impacto en componentes existentes + +| Unidad | Cambio futuro acotado | +| --- | --- | +| `CompaniesWorkspace` | Coordinar modo global, cabecera compacta, franja y scroll principal; conservar contratos de datos y rutas. | +| `WorkspaceSidebar` y preferencia | Reutilizar destinos; separar intención guardada y modo efectivo; presentación permanente/Sheet sin duplicar enlaces focusables. | +| `WorkspaceHeader` | Activador global y búsqueda compacta; mantener ámbito de búsqueda y cuenta. | +| `WorkspaceNavigationButton` | Conservar BrowserLink, nombres y activo; tooltip real al foco en raíl. | +| `CompanyDetail` | Extraer navegación contextual por responsabilidad, mantener 21 destinos/contadores y edición; grid permanente u overlay. | +| `AssetInventoryPage` y controlador | Referencia estable de capacidad, presentación única de detalle, conservación del estado local y tabla. | +| `ResourcesPage` y controlador | Aplicar política equivalente tras comprobar diferencias de selección/ruta; presupuesto propio validado con fixture de recurso. | +| `SelectionPanel` | Consumir modo/layout explícito, eliminando dependencia rígida de viewport solo en consumidores migrados. Buscar todos los usos antes de cambiar contrato. | +| `useCompactDetail` | Sustituir usos de activos/recursos por capacidad; retirar solo si búsqueda completa demuestra que no quedan consumidores. | +| `AssetSelectionPanel`, diálogos y contenido de detalle | Reutilizar presentación y acciones; foco y lectura al migrar. La ficha completa permanece independiente. | +| `apps/web/src/global.css` | Tokens de capacidad/alto compacto y variantes propias después de aprobación; no alterar colores ni vendor. `dashboard` no cambia por V01. | +| Traducciones es/en | Activadores, cierre, fijar/ocultar, etiquetas y explicaciones breves de capacidad. | + +Posibles extracciones: navegación contextual de compañía y overlay global en sus ficheros dentro del módulo, y hook genérico de capacidad con consumidores reales activos/recursos en rdx-ui. Mantener rutas, consultas y DTO en módulos. No introducir un megacomponente con flags para global/compañía/listado/modal. No hace falta envolver Sheet si la composición propia del módulo basta. + +## 12. Plan de implementación incremental + +1. **Aprobar presupuestos y contratos.** Confirmar arquitectura B híbrida, hipótesis 800/1144, reglas de preferencias y franja sticky. Retener las mediciones como baseline. Sin aprobación no se aplica la propuesta. +2. **Navegación móvil y franja contextual.** Extraer lista de destinos sin cambiarlos; Sheet global/contextual, títulos, foco, salto al contenido y edición de compañía. Conservar escritorio existente en esta primera entrega. Verificar ocho tamaños, 21 destinos, es/en y URLs directas. Reversión: revertir esta entrega, sin migraciones. +3. **Composición de portátil/tablet.** Modo global compacto y secundaria por capacidad; intención persistida separada de modo, resize y overlays. Depende del paso 2. Probar localStorage ausente, inválido y bloqueado, estados expandidos/contraídos y regreso a escritorio. Revertible sin tocar datos; ignorar nueva preferencia si se retira. +4. **Capacidad tabla/detalle de activos.** Introducir hook sobre inventario, presentación única y sincronía foco/scroll. Eliminar la doble decisión CSS/media query para este consumidor. Comparar a ambos lados del umbral y con navegaciones ocultas/fijas. No cambiar rutas de ficha ni selección local. +5. **Consumidor equivalente de recursos.** Migrar su composición con fixture sintético autorizado, sin cambiar semántica de negocio; comprobar selección en URL/local y cierre. No darlo por validado por compartir componente con activos. +6. **Regresión y documentación normativa.** Playwright, tests dirigidos y comprobaciones globales según AGENTS para cambio transversal. Revisar todos los consumidores, dark mode, zoom, foco y consola. Solo entonces actualizar DESIGN.md y los patrones con el comportamiento aprobado e implementado. + +Pruebas unitarias de contratos: modo en límites y cambios de capacidad, preferencias y fallbacks, apertura/cierre y retorno de foco; no tests que repliquen clases CSS. Pruebas de navegador: tabla/scroll sin remount, selección y URL estables, editor anidado, búsqueda global, compañías con nombres largos y varias compañías. Build/tipos/lint/tests no se ejecutan para este documento porque no se modifica código; serán necesarios al implementar. + +Separación de alcance: V01 no ajusta ayuda/progreso de asistentes (V04), estado al volver de ficha (V05), búsqueda (V07), selector de columnas (V08), detalle vacío (V10) ni estados de carga (V12). Las correcciones previas V02/V03/V06/V09 se conservan y se someten a regresión si sus componentes resultan afectados. + +## 13. Criterios de aceptación + +Son criterios propuestos para implementación futura; ninguno se declara superado por la auditoría actual. + +| Criterio | Comprobación Playwright y origen | +| --- | --- | +| Contenido inmediato en móvil | A 390/320, navegación cerrada, `main.y ≤ 128 px` tras carga. Presupuesto: cabecera compacta 56 + contexto de hasta 72. Título del listado visible sin scroll; no depender de contar gestos. Validar primero con textos sintéticos actuales y luego nombres largos/es/en. | +| Orientación persistente | Compañía y sección visibles al cargar y tras scroll de contenido; accesibles completas aunque el texto visual se trunque. En ficha, sección Activos más nombre del activo en contenido. | +| Sin desbordamiento del shell | `documentElement.scrollWidth ≤ clientWidth + 1` en las ocho resoluciones, con y sin overlay/panel, nombres largos y zoom. Margen de 1 px por redondeo CSS. No exigirlo al contenido interior desplazable de tabla. | +| Todos los destinos | Comparar href/nombre de los 21 enlaces de compañía antes/después, incluidos últimos destinos; navegar uno por grupo, URL directa, recarga y Atrás/Adelante. Repetir con varias compañías y roles disponibles. | +| Presupuesto tabla/panel | Cuando `inline`, ancho de listado ≥800 px y viewport de tabla aproximadamente ≥798 px, sujeto a bordes reales; `listado + gap + panel ≤ W`. Cuando no quepa, overlay y tabla usando todo W. Validar ±1 px del límite 1144 y sidebar expandida/compacta/oculta. | +| Tablet con contenido útil | A 768 con valores propuestos, contenido interior cercano a 736 px; no reservar las columnas 228+240. Esta cifra es objetivo calculado y debe ajustarse si cambian bordes/scrollbars, conservando el principio de ancho completo. | +| Escritorio amplio conservado | A 1920, ambas navegaciones y panel utilizables; tabla no menor que baseline ≈977 px con tolerancia de 2 px por bordes. No aplicar colapso automático innecesario. | +| Cambio de modo sin pérdida | Filtrar, ordenar, paginar cuando haya datos, desplazar tabla, seleccionar y redimensionar: mismos valores, ID y nodo de tabla; scroll conservado salvo clamp natural al crecer viewport. No generar entradas de historial por resize. | +| Modalidad y foco | Tab/Shift+Tab contenidos, Escape/cierre devuelven foco, fondo bloqueado; cambio de destino enfoca título. Resize libera modalidad y mantiene foco visible según §9; no existen dos copias focusables de navegación/detalle. | +| Acciones y enlaces | Enter en enlaces, Espacio en botones; Ctrl/Cmd/medio/target conservados. Enlaces de relación y acciones de fila sin interacciones anidadas. | +| Preferencias | Recarga conserva intención manual; estrechar/ampliar no escribe colapso automático. localStorage inaccesible no rompe navegación. | +| Accesibilidad ampliada | Zoom real 200/400 %, reducción de movimiento, teclado completo, comprobación con lector y contrastes de estados nuevos. No certificar WCAG global por superar solo la matriz geométrica. | + +El mínimo 800 y los límites derivados quedan sujetos a aprobación y validación de contenido representativo; no convertirlos en norma definitiva sin esa comprobación. + +## 14. Riesgos y cuestiones abiertas + +Decisiones que requieren aprobación antes de desarrollo: + +1. Arquitectura B híbrida y prioridad automática del contenido sobre la expansión permanente de navegación. +2. Presupuesto inicial de tabla 800, panel 320–400 y límites derivados 920/1160/1660; validar con datos densos antes de fijarlos. +3. Franja contextual sticky de 48–72 px y cabecera compacta de 56; aceptar un botón de búsqueda en móvil conservando su ámbito actual. +4. Si persistir la intención de fijar/ocultar navegación de compañía o mantenerla solo durante la sesión; recomendación: empezar en memoria y añadir persistencia únicamente si se confirma su utilidad. Mantener desde el inicio la preferencia global existente. + +Riesgos técnicos: pérdida de foco al cambiar árbol modal, scroll bloqueado después de resize, reinicio de tabla al reorganizar layout, desincronización CSS/React, clases `inventory:` supervivientes y diferencias de rutas entre activos/recursos. Mitigar con una única decisión por frontera, composición estable y pruebas específicas. + +Limitaciones de validación: una compañía y un activo sintéticos, sin inventarios grandes ni recursos; no se probaron usuarios con otros roles, muchas compañías, traducción inglesa, nombres arbitrariamente largos, zoom real o lector de pantalla en esta tarea. No existe prototipo para medir aprendizaje o tiempos. Esos límites no impiden confirmar V01, pero sí fijar números definitivos o declarar accesibilidad completa. + +La documentación de componentes contiene descripciones históricas que ya no coinciden con todas las APIs instaladas, por ejemplo «no hay Avatar ni DropdownMenu»; se contrastó con código real y no se corrigió fuera de alcance. El patrón de listados describe navegación escritorio/modal móvil vigente: tras aprobar e implementar habrá que precisar su nueva condición por capacidad, sin modificar ahora la norma. + +Actualizaciones futuras de DESIGN.md: **§4 Layout y navegación**, **§6 Fichas** (panel/diálogo), **§9 Accesibilidad** (salto y foco), **§10 Responsive y densidad**, **§11 deuda/validación**. En §3 solo registrar tokens propios nuevos si se introducen. Actualizar además las secciones Shell global y Panel lateral de componentes, el apartado responsive de listados y la descripción de umbral en rutas; no cambiar los href ni contratos HTTP. + +## 15. Evidencias y referencias + +### Artefactos de esta tarea + +- [Mediciones de los 48 casos](../.artifacts/playwright/v01-design/measurements.json): rectángulos de navegación, main, tabla, panel/diálogo, visibilidad, scroll y overflow. +- [Interacciones adicionales](../.artifacts/playwright/v01-design/interactions.json): colapso, recuperación tras recarga y desplazamiento móvil. +- [Consola autenticada](../.artifacts/playwright/v01-design/console.json). +- [Índice de capturas](../.artifacts/playwright/v01-design/evidence-index.json): dimensiones y SHA-256. +- Snapshots de seis escenarios y capturas por escenario/ancho en `.artifacts/playwright/v01-design/`. Los artefactos están excluidos de Git; su enlace es local. Para revisión en otro equipo hay que entregar este directorio de evidencias, **sin incluir ningún directorio `authenticated-*` ni archivos de sesión**. El documento conserva las medidas principales para ser legible sin imágenes. + +Para reproducir: reutilizar una sesión sintética activa del preparador siguiendo [Playwright MCP](playwright-mcp.md), abrir los seis destinos del informe de ese entorno, usar las ocho resoluciones de §4 y medir tras carga/fuentes/transiciones. Una sesión caducada requiere volver a autorizar/preparar el entorno correspondiente; no utilizar datos reales como sustituto. Los orquestadores temporales de medición se conservan en el directorio de evidencias para inspección; no son infraestructura de producción. + +### Fuentes locales realmente consultadas + +- [AGENTS.md](../AGENTS.md), [DESIGN.md](../DESIGN.md), [auditoría visual, V01](auditoria-ui-ux-visual.md#v01--el-layout-reserva-demasiado-espacio-a-la-navegación), [listados](patron-de-listados-de-entidades.md), [diálogos](patron-de-dialogos.md), [rutas](rutas-del-cliente.md), [componentes](componentes-de-interfaz.md), [procedimiento MCP](playwright-mcp.md) y [preparador](../scripts/visual-audit.py). +- [Tokens efectivos](../apps/web/src/global.css), [CompaniesWorkspace](../modules/companies/src/web/workspace/view/ui/companies-workspace.tsx), [WorkspaceSidebar](../modules/companies/src/web/workspace/view/ui/workspace-sidebar.tsx), [WorkspaceHeader](../modules/companies/src/web/workspace/view/ui/workspace-header.tsx), WorkspaceContent, WorkspaceNavigationButton, preferencia global y controlador de workspace en sus directorios actuales. +- [CompanyDetail](../modules/companies/src/web/companies/detail/ui/company-detail.tsx), su controlador, [rutas reales](../modules/companies/src/web/workspace/shared/helpers/workspace-routes.ts), [BrowserLink](../modules/core/src/web/navigation/shared/ui/browser-link.tsx). +- [Inventario de activos](../modules/assets/src/web/assets/list/ui/inventory-page.tsx), [controlador](../modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts), [panel](../modules/assets/src/web/assets/detail/ui/asset-selection-panel.tsx), [ficha completa](../modules/assets/src/web/assets/detail/ui/asset-detail-page.tsx), ResourcesPage y su controlador como consumidor equivalente. +- [useCompactDetail](../packages/rdx-ui/src/hooks/use-compact-detail.ts), [SelectionPanel](../packages/rdx-ui/src/components/selection-panel.tsx), EntityTable/EntityTableContent y API pública de [Sheet](../packages/shadcn-ui/src/components/sheet.tsx). Vendor consultado, sin modificaciones. + +### Skills y aplicación al análisis + +| Skill leída | Referencias aplicables consultadas | Influencia concreta | +| --- | --- | --- | +| [frontend-design](../.agents/skills/frontend-design/SKILL.md) | Principios, planificación/wireframes y revisión contenidos en SKILL.md | Mantener identidad Inter/verde/turquesa/rojo y jerarquía operativa; evitar decoración o nuevo sistema visual. | +| [shadcn](../.agents/skills/shadcn/SKILL.md) | `rules/composition.md`, `rules/base-vs-radix.md`, `rules/styling.md` | Sheet/Dialog existentes, títulos accesibles, composición `render` de Base UI y tokens semánticos. No aplicar sus procedimientos de regeneración vendor, incompatibles con AGENTS y esta tarea. | +| [vercel-composition-patterns](../.agents/skills/vercel-composition-patterns/SKILL.md) | `architecture-avoid-boolean-props.md`, `state-context-interface.md` | Separar destinos/estado de su presentación; evitar megacomponente con múltiples flags y conocimiento de negocio en rdx-ui. | +| [vercel-react-best-practices](../.agents/skills/vercel-react-best-practices/SKILL.md) | `client-event-listeners.md`, `rerender-derived-state-no-effect.md` | Suscripciones por frontera, modo discreto y preferencia separada del modo derivado; no duplicar estado remoto ni añadir SWR por el ejemplo de la skill. | + +No se han aplicado skills inexistentes ni recomendaciones específicas de Next.js/RSC a esta SPA. Las recomendaciones estéticas generales ceden ante los patrones de Metis. Las tres referencias externas de §9/10 se consultaron para contrastar viabilidad; las medidas y conclusiones sobre Metis proceden del repositorio y del navegador. + +### Verificación de entrega + +Se revisaron estructura, coherencia de wireframes/contratos, enlaces locales, evidencias y diff. La comparación con el estado inicial conserva los cambios previos del sprint sin alterarlos. Única modificación versionable de esta tarea: este documento. No se modificaron componentes, CSS, tokens, rutas, API, autenticación, vendor ni DESIGN.md. V01 y los demás hallazgos conservan su estado previo. diff --git a/modules/assets/src/web/assets/list/ui/inventory-page.tsx b/modules/assets/src/web/assets/list/ui/inventory-page.tsx index 5165e82..9919f78 100644 --- a/modules/assets/src/web/assets/list/ui/inventory-page.tsx +++ b/modules/assets/src/web/assets/list/ui/inventory-page.tsx @@ -6,6 +6,7 @@ import { } from "@repo/rdx-ui/components/entity-list"; import { BoxIcon, PencilIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { CatalogError } from "../../../catalog"; @@ -20,12 +21,16 @@ import type { AssetInventoryPageProps } from "../types"; import { AssetWorkstationAction } from "./asset-workstation-action"; function selectedRowClass(id: string, selectedId: string | undefined, detailOpen: boolean) { + // El primario contextual mantiene legibles todos los enlaces y sus focos sobre la selección. return id === selectedId && detailOpen - ? "bg-accent hover:bg-accent [&>[data-pinned=right]]:bg-accent [&>[data-pinned=right]]:group-hover/entity-row:bg-accent [&>[data-pinned=right]]:group-has-aria-expanded/entity-row:bg-accent" - : ""; + ? "bg-accent hover:bg-accent [--muted-foreground:var(--accent-foreground)] [--primary:var(--accent-foreground)] [--ring:var(--accent-foreground)] [&>[data-pinned=right]]:bg-accent [&>[data-pinned=right]]:group-hover/entity-row:bg-accent [&>[data-pinned=right]]:group-has-aria-expanded/entity-row:bg-accent" + : "hover:[--primary:var(--accent-foreground)] hover:[--ring:var(--accent-foreground)]"; } export function AssetInventoryPage(props: AssetInventoryPageProps) { + const inventoryRef = useRef(null); + const newAssetButtonRef = useRef(null); + const editorTriggerRef = useRef(null); const { companyId, lookups } = props; const { query, @@ -59,6 +64,34 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { } = useAssetInventoryPageController(props); const { t, i18n } = useTranslation("assets"); + function openEditor(id: string) { + // Se captura antes de que el anuncio de progreso del asistente reciba el foco. + editorTriggerRef.current = + document.activeElement instanceof HTMLElement && document.activeElement !== document.body + ? document.activeElement + : null; + setEditor(id); + } + + function editorFinalFocus() { + const trigger = editorTriggerRef.current; + + if (trigger?.isConnected && !trigger.matches(":disabled, [aria-disabled=true]")) { + return trigger; + } + const newAssetButton = newAssetButtonRef.current; + + if (newAssetButton?.isConnected && !newAssetButton.disabled) { + return newAssetButton; + } + + return ( + inventoryRef.current?.querySelector("input[data-quick-search]:enabled") ?? + inventoryRef.current ?? + true + ); + } + if (missingView) { return ; } @@ -68,6 +101,8 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { className="grid grid-cols-[minmax(0,1fr)] items-start gap-6 inventory:grid-cols-[minmax(0,1fr)_clamp(320px,24vw,400px)] inventory:data-[detail-open=false]:grid-cols-[minmax(0,1fr)]" data-detail-open={detailOpen && Boolean(selected)} data-slot="inventory-layout" + ref={inventoryRef} + tabIndex={-1} >
)} - @@ -147,7 +186,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { primaryAction={{ label: t("edit"), icon: