feat: restructure frontend documentation and introduce new architectural guidelines

- Removed outdated documents on screen design and editor layout audit.
- Added comprehensive documentation on frontend architecture, UI patterns, and UI system.
- Established clear guidelines for component usage, routing, and layout structures.
- Enhanced accessibility and responsiveness standards across UI components.
- Defined testing protocols and anti-patterns to maintain code quality and consistency.
This commit is contained in:
David Arranz 2026-09-04 18:52:45 +02:00
parent 3aa3f4018b
commit 5ad446be00
16 changed files with 517 additions and 2583 deletions

View File

@ -4,7 +4,7 @@ Aplicación frontend principal del ERP.
## Documentación canónica
- Flujo de autenticación y selección de empresa: [`../../docs/frontend/auth-and-company-selection.md`](../../docs/frontend/auth-and-company-selection.md)
- Arquitectura, autenticación y selección de empresa: [`../../docs/frontend/frontend-architecture.md`](../../docs/frontend/frontend-architecture.md)
- Contexto tenant y reglas de `X-Company-Id`: [`../../docs/architecture/tenant-context.md`](../../docs/architecture/tenant-context.md)
- Tratamiento de errores HTTP en frontend: [`../../docs/architecture/http-error-handling-frontend.md`](../../docs/architecture/http-error-handling-frontend.md)
@ -14,13 +14,13 @@ La app usa `@erp/identity/client` como runtime actual.
## Flujo actual
- La sesión y la empresa activa se resuelven según el flujo documentado en `docs/frontend/auth-and-company-selection.md`.
- La sesión y la empresa activa se resuelven según el flujo documentado en `docs/frontend/frontend-architecture.md`.
- Las rutas tenant-scoped deben respetar las reglas de `X-Company-Id` documentadas en `docs/architecture/tenant-context.md`.
## Headers
- No duplicar aquí la matriz de headers.
- Consultar `docs/frontend/auth-and-company-selection.md` para endpoints excluidos y `tenant-context.md` para reglas tenant-scoped.
- Consultar `docs/frontend/frontend-architecture.md` para endpoints excluidos y `tenant-context.md` para reglas tenant-scoped.
## App company switcher
@ -42,10 +42,10 @@ Reglas:
- no llamar directamente a `GET /companies/available`
- no usar `companySlug` para auth
- no modificar el access token
- respetar el flujo canónico documentado en `docs/frontend/auth-and-company-selection.md`
- respetar el flujo canónico documentado en `docs/frontend/frontend-architecture.md`
## Refresh automático
La app implementa refresh automático global sobre `401`.
La semántica completa de retry, exclusiones y limpieza de sesión se documenta en `docs/frontend/auth-and-company-selection.md`.
La semántica completa de retry, exclusiones y limpieza de sesión se documenta en `docs/frontend/frontend-architecture.md`.

View File

@ -11,7 +11,7 @@ Los endpoints, payloads y rutas aquí recogidos pertenecen a una fase anterior d
- Auth y sesión: [architecture/identity-auth-api.md](./architecture/identity-auth-api.md)
- Contexto tenant: [architecture/tenant-context.md](./architecture/tenant-context.md)
- Relación `identity` / `companies`: [architecture/identity-and-companies.md](./architecture/identity-and-companies.md)
- Frontend auth: [frontend/auth-and-company-selection.md](./frontend/auth-and-company-selection.md)
- Frontend auth: [frontend/frontend-architecture.md](./frontend/frontend-architecture.md)
- Customer invoices y proformas: [customer-invoices/README.md](./customer-invoices/README.md)
## Motivo de conservación

View File

@ -25,9 +25,9 @@ Este directorio reúne la documentación técnica, operativa y de apoyo del ERP.
#### Frontend
- [Flujo de auth y selección de empresa](./frontend/auth-and-company-selection.md)
- [Diseño de pantallas de listado](./frontend/diseno-pantallas-listado.md)
- [Diseño de cuadros de diálogo de formulario](./frontend/diseno-cuadros-dialogo.md)
- [Arquitectura frontend](./frontend/frontend-architecture.md)
- [Sistema UI Rdx](./frontend/ui-system.md)
- [Patrones de UI](./frontend/ui-patterns.md)
- [Sistema modular de Settings](./settings/frontend-erp-settings-system-design.md)
- [Integración de módulos con Settings](./settings/frontend-erp-settings-module-integration-design.md)
- [README de `apps/web`](../apps/web/README.md)

View File

@ -20,7 +20,7 @@ Este documento resume requisitos funcionales tempranos del ERP. No define el run
- Arquitectura y tenant context: [architecture/README.md](./architecture/README.md)
- Auth actual: [architecture/identity-auth-api.md](./architecture/identity-auth-api.md)
- Flujo frontend: [frontend/auth-and-company-selection.md](./frontend/auth-and-company-selection.md)
- Arquitectura y flujo frontend: [frontend/frontend-architecture.md](./frontend/frontend-architecture.md)
- Módulos documentales: [customer-invoices/README.md](./customer-invoices/README.md) y [document-series/README.md](./document-series/README.md)
## Nota

View File

@ -3,7 +3,7 @@
## Referencias canónicas
- Reglas de contexto tenant y middlewares públicos: [tenant-context.md](./tenant-context.md)
- Flujo frontend de autenticación y selección de empresa: [../frontend/auth-and-company-selection.md](../frontend/auth-and-company-selection.md)
- Flujo frontend de autenticación y selección de empresa: [../frontend/frontend-architecture.md](../frontend/frontend-architecture.md)
- Runtime y responsabilidades del módulo `identity`: [../../modules/identity/README.md](../../modules/identity/README.md)
## Migrado a `identity`
@ -69,7 +69,7 @@ No existe todavía enforcement real de `identity:companies:update`.
El runtime nuevo ya usa `@erp/identity/client`.
Los detalles del flujo implementado, headers y refresh automático se mantienen en `frontend/auth-and-company-selection.md`.
Los detalles del flujo implementado, headers y refresh automático se mantienen en `frontend/frontend-architecture.md`.
## Legacy pendiente

View File

@ -1,103 +0,0 @@
# Contributing UI
## Reglas base
- todos los componentes públicos propios deben usar prefijo `Rdx`;
- el código y los identificadores deben estar en inglés;
- la documentación y los comentarios deben estar en español;
- usar TypeScript estricto;
- usar imports de tipos explícitos en cabecera;
- no usar tipos inline tipo `import("react").ReactNode`.
## Responsabilidad de `packages/rdx-ui`
Los componentes de `rdx-ui`:
- no conocen React Query;
- no conocen routing;
- no conocen DTOs;
- no conocen entities;
- no conocen controllers funcionales;
- no contienen lógica de negocio.
Deben recibir props ya preparadas por la capa consumidora.
## Tema y color
- todo debe funcionar en `light` y `dark` desde el inicio;
- usar tokens semánticos;
- no introducir colores físicos sin justificación fuerte y documentada;
- no usar el color como único indicador de estado.
## Accesibilidad
- foco visible obligatorio;
- botones de icono con nombre accesible;
- no crear `div` clicables;
- mantener orden DOM coherente;
- usar controles reales proporcionados por el consumidor;
- evitar wrappers vacíos que compliquen la semántica.
## API pública
- mantener una API pequeña y explícita;
- no crear abstracciones genéricas sin repetición real;
- `className` es un escape controlado, no la API principal;
- no usar `transition-all` en nuevos componentes salvo caso excepcional documentado.
## Composición de paneles
- no duplicar sombras entre una primitive y su composición;
- no duplicar el control de cierre entre capas;
- las acciones de contexto deben llegar ya construidas desde el consumidor;
- los labels accesibles de acciones de dominio pertenecen al consumidor;
- una primitive reutilizable puede resolver superficie e infraestructura, mientras la composición resuelve jerarquía visual y accesibilidad contextual.
## Acciones de fila
- `RdxRowActions` solo renderiza acciones primarias y overflow;
- el consumidor decide qué acciones existen y en qué orden aparecen;
- las acciones destructivas deben ir en overflow salvo excepción justificada por código real;
- `disabledReason` pertenece al consumidor y debe ser comprensible sin conocimiento de dominio en `rdx-ui`;
- no mover permisos, navegación, dialogs ni entidades dentro de `packages/rdx-ui`;
- los icon buttons deben mantener `aria-label` y tooltip accesible.
## Editor simple
- por defecto, las pantallas cuya finalidad principal sea crear, editar o configurar mediante formulario deben usar shell `app-fullscreen` y no el shell con sidebar;
- usar `PageFormHeader` o el header de dominio ya existente para la navegación mínima de salida, sin duplicar una segunda navegación lateral;
- `RdxPanel` con `variant="plain"` es el patrón por defecto del editor simple;
- usar una única superficie editable cuando sea útil separar claramente la página del área de formulario;
- no envolver cada sección en `Card` por defecto;
- no duplicar padding entre shell, layout y superficie editable;
- `RdxFormGrid` debe concentrar la retícula del editor sin acoplarse a RHF;
- las diferencias entre create y update no justifican nuevos componentes si solo cambian defaults o wiring.
## Editor complejo
- `RdxEditorPageLayout` debe reservar el `aside` para resumen, metadata o contexto de apoyo;
- no mover campos editables al `aside` solo para equilibrar visualmente columnas;
- en desktop el layout objetivo del editor complejo es `minmax(0, 1fr) ~22rem`;
- el `aside` debe apilarse debajo del `main` cuando no haya ancho suficiente;
- si no existe `aside`, no debe reservarse hueco lateral;
- mantener una única superficie principal para el formulario, también en editores complejos;
- `RdxPanel` puede usar `variant="card"` dentro del `aside` cuando el bloque necesite superficie independiente;
- `RdxFormGrid` debe quedarse en grupos de campos y no envolver tablas, line editors ni layouts funcionales especializados;
- no mover los totales al `aside` por defecto;
- si el editor usa footer sticky inferior, no duplicar `Guardar` y `Cancelar` completos en el header;
- `RdxEditorStickyFooter` no conoce RHF, submit, routing ni dominio; solo compone superficie, orden, separación y responsive;
- la región `leading` del footer puede alojar resumen o contexto, pero ese contenido sigue perteneciendo al dominio consumidor;
- coordinar cualquier `sticky` lateral con el offset del header para evitar clipping y foco oculto.
## Sincronización obligatoria
Todo cambio futuro que afecte a la API pública o al comportamiento visual debe actualizar en el mismo cambio:
```txt
1. implementación
2. documentación Markdown
3. página interactiva UI Kit
4. tests aplicables
```
Un cambio queda incompleto si cualquiera de esas cuatro piezas queda desincronizada.

View File

@ -1,134 +0,0 @@
# Foundations Rdx
## Estado
Este documento define las foundations estructurales iniciales del sistema visual Rdx.
El documento canónico de contexto global es:
- [visual-system-design.md](./visual-system-design.md)
## Espaciado de página
```txt
mobile:
padding 4
desktop:
padding 6
```
Referencia inicial:
```txt
px-4 py-4
md:px-6 md:py-6
```
Se aplica en `RdxPageContainer`.
Uso correcto:
- usarlo como padding principal de página;
- no anidarlo dentro de otro contenedor que ya aporte el mismo padding estructural;
- si el shell ya aplica padding equivalente, el consumidor debe evitar duplicarlo.
## Ritmo vertical
```txt
compact:
gap 4
default:
gap 6
relaxed:
gap 8
```
No se fija `gap-12` como foundation general.
Se aplica en `RdxPageStack`.
## Anchuras
```txt
full:
w-full
wide:
max-w-screen-2xl mx-auto
content:
max-w-5xl mx-auto
```
Uso recomendado:
- `full`: listados, tablas, editores complejos, pantallas documentales;
- `wide`: detalles o páginas amplias centradas;
- `content`: editores simples y formularios de una columna lógica.
## Superficies
```txt
contenido principal:
plano
secciones:
separadas por espacio o divisores
aside:
puede usar paneles
tabla/editor de líneas:
puede usar superficie independiente
card:
excepción, no default
```
Reglas:
- no usar card como solución por defecto para toda la página;
- evitar card dentro de card;
- preferir espacio, jerarquía tipográfica y divisores.
## Sombras
```txt
página:
sin sombra
secciones:
sin sombra
panel contextual:
sombra suave opcional
sticky header/footer:
preferir borde
usar sombra mínima solo cuando sea necesaria
```
## Tema
Uso obligatorio de tokens semánticos:
```txt
bg-background
text-foreground
bg-muted
text-muted-foreground
border-border
bg-primary
text-primary-foreground
bg-destructive
text-destructive-foreground
```
Reglas:
- no introducir colores físicos para resolver superficies o estados;
- no crear una nueva paleta de `success`, `warning` o `info` en esta fase si no existe convención central consolidada;
- validar cualquier foundation nueva en `light` y `dark`.

View File

@ -1,260 +0,0 @@
# Page Layouts Rdx
## Estado
Este documento describe los patrones conceptuales de página aprobados para adopción progresiva.
El documento canónico de contexto global es:
- [visual-system-design.md](./visual-system-design.md)
## Listado
```txt
RdxPageContainer
└── RdxPageStack
├── RdxPageHeader
├── notices opcionales
└── contenido del listado
```
Reglas:
- anchura por defecto `full`;
- acciones principales en cabecera;
- acciones de fila frecuentes visibles y acciones destructivas en overflow mediante `RdxRowActions`;
- panel contextual solo cuando aporte contexto real;
- la composición del listado no debe acoplarse a React Query, routing ni dominio.
Ejemplo de integración real validado en `proformas/list` y `customers/list`:
```tsx
<RdxPageContainer width="full">
<RdxPageStack spacing="default">
<RdxPageHeader
title={title}
description={description}
actions={<RdxPageActions primary={primaryAction} />}
/>
{notice}
{content}
</RdxPageStack>
{dialogs}
</RdxPageContainer>
```
Aclaraciones:
- `RdxPageContainer` resuelve el padding exterior y la anchura de página;
- `RdxPageStack` resuelve el ritmo vertical principal entre cabecera, aviso y contenido;
- `RdxPageHeader` no conoce navegación ni callbacks de React Router;
- la tabla, los filtros, los panels, los dialogs y los controllers permanecen fuera de las foundations;
- `RdxRowActions` puede vivir dentro de las columnas de tabla, pero recibe acciones ya decididas por el consumidor;
- en ambos pilotos el panel contextual sigue viviendo en composición local mediante `ResizablePanelGroup`;
- no documentar todavía `RdxListPageLayout` ni `RdxResponsivePageGrid` mientras el resize y la apertura del aside dependan del caso de uso.
### Evidencia de pilotos de listado
```txt
DECISIÓN_CONFIRMADA
Container + Stack + Header + Actions cubren la capa exterior del listado.
DECISIÓN_PROVISIONAL
La Page puede extraer una vista pura si mejora el test y reduce wiring.
NO_ABSTRAER_TODAVÍA
main + aside redimensionable
shell específico del panel contextual
toolbars
```
## Listado con panel contextual
```txt
ResizablePanelGroup
├── main
└── RightPanel
└── RdxContextPanel
```
Reglas:
- `ResizablePanelGroup` gestiona la geometría, el resize y la persistencia local del split;
- `RightPanel` gestiona la superficie lateral, la altura y el overflow base;
- `RdxContextPanel` gestiona cabecera, cierre, body y footer visual del panel;
- el contenido interno del panel sigue siendo específico del dominio;
- no documentar todavía un layout genérico de listado a partir de este patrón.
## Editor simple
```txt
scroll container
└── RdxEditorPageLayout complexity="simple"
└── form stack
├── RdxPanel
├── RdxPanel
└── ...
```
Reglas:
- las rutas cuya tarea principal sea crear, editar o configurar mediante un formulario focalizado deben entrar por el shell `app-fullscreen`;
- listados, dashboards, detalle y navegación de settings mantienen `app-sidebar` salvo necesidad funcional explícita;
- `RdxEditorPageLayout` resuelve padding interior, ancho centrado y `min-w-0`;
- el editor simple puede apoyarse en una única superficie contenedora para diferenciar la página del área editable, sin volver a cards por sección;
- `RdxPanel` expresa jerarquía por defecto con spacing y divisor inferior;
- `RdxFormGrid` mantiene la retícula del formulario sin conocer RHF ni dominio;
- priorizar lectura y velocidad de escaneo;
- usar secciones planas con espacio o divisores;
- reservar sticky footer solo cuando el scroll lo justifique.
Ejemplo validado en `customers/update`, `customers/create` y `/account`:
```tsx
<AppFullscreenLayout>
<PageFormHeader backLabel={backLabel} onBack={onBack} title={title} />
<div className="flex-1 overflow-y-auto">
<RdxEditorPageLayout complexity="simple">
<div className="flex min-w-0 flex-col gap-6">
{notice}
<form className="flex min-w-0 flex-col gap-6">
<RdxPanel title={title} description={description}>
<RdxFormGrid>{fields}</RdxFormGrid>
</RdxPanel>
</form>
</div>
</RdxEditorPageLayout>
</div>
</AppFullscreenLayout>
```
Deuda registrada:
- `PageFormHeader` sigue siendo full-width mientras el contenido del editor queda centrado en `max-w-5xl`;
- no modificar todavía el header legacy sin validación visual manual real.
Evidencia adicional de create/update:
```txt
DECISIÓN_CONFIRMADA
ambos flujos reutilizan el mismo layout, superficie y secciones.
ESPECÍFICO
defaults iniciales
wiring de submit
feedback de error
navegación de retorno concreta
```
Candidatos explícitos para auditoría posterior:
- `customers/create`
- `customers/update`
- `/account`
- `proformas/update`
## Editor complejo
```txt
shell
├── PageFormHeader
├── scroll container
│ └── RdxEditorPageLayout complexity="complex"
│ ├── main
│ │ └── única superficie editable
│ │ ├── RdxPanel
│ │ ├── RdxPanel
│ │ ├── bloque de líneas con layout propio
│ │ └── ...
│ └── aside
│ └── controles secundarios o contexto de apoyo
└── RdxEditorStickyFooter
├── summary/context region
└── action region
```
Reglas:
- `RdxEditorPageLayout` resuelve padding exterior, `min-w-0` y la geometría `main + aside`;
- el `main` debe alojar los campos editables y sus bloques funcionales;
- el `aside` debe reservarse para metadata, configuración o contexto de apoyo ya existente en la pantalla;
- el `aside` del editor complejo usa una geometría aproximada `minmax(0, 1fr) 22rem` en desktop y se apila debajo del `main` cuando no hay ancho suficiente;
- no mover campos editables al `aside` solo por estética si eso empeora el flujo;
- no mover los totales al `aside`;
- los bloques con layout interno propio, como una tabla de líneas, no deben forzarse dentro de `RdxFormGrid`;
- `RdxEditorStickyFooter` resuelve una única superficie sticky con una región contextual flexible y una región de acciones;
- los totales siguen perteneciendo al dominio de Proformas aunque se rendericen dentro de la región contextual del footer;
- `PageFormHeader` puede seguir existiendo, pero no debe duplicar `Guardar` y `Cancelar` cuando el footer sticky ya los contiene.
Ejemplo validado en `proformas/update`:
```tsx
<div className="fixed inset-0 flex flex-col overflow-hidden">
<PageFormHeader />
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
<div className="min-h-0 flex-1 overflow-y-auto">
{notice}
<RdxEditorPageLayout
complexity="complex"
aside={aside}
>
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<form className="flex min-w-0 flex-col gap-6">
<RdxPanel title={documentTitle}>
<RdxFormGrid>{documentFields}</RdxFormGrid>
</RdxPanel>
{lineEditor}
</form>
</div>
</RdxEditorPageLayout>
</div>
<RdxEditorStickyFooter
leading={<TotalsSummary />}
secondary={cancelButton}
primary={saveButton}
status={status}
/>
</div>
</div>
```
Evidencia del piloto:
```txt
DECISIÓN_CONFIRMADA
main contiene la única superficie editable
aside contiene bloques secundarios ya existentes del editor
la sticky zone inferior pasa a una única superficie visual
los totales permanecen como dominio de Proformas dentro de la región contextual
las acciones principales comparten el mismo footer sin una segunda franja
RdxFormGrid se mantiene solo para campos
DECISIÓN_PROVISIONAL
visual exacta del header legacy frente al ancho full del editor complejo
NO_ABSTRAER_TODAVÍA
layout específico del bloque de líneas
layout específico del footer de totales
```
## Detalle
```txt
width="wide" o "full"
main + aside opcional
aside para metadata, actividad, resumen o acciones
```
Reglas:
- mantener patrón consistente de label/value;
- usar aside solo cuando el contenido realmente gane claridad;
- no implementar todavía `RdxDetailPageLayout` en esta fase.

File diff suppressed because it is too large Load Diff

View File

@ -1,215 +0,0 @@
# Frontend Auth And Company Selection
## Runtime actual
El frontend ERP usa el runtime nuevo:
```txt
@erp/identity/client
IdentityAuthSessionProvider
DataSourceProvider
Axios compartido
```
No debe usarse `@erp/auth/client` como runtime nuevo.
## Flujo actual
```txt
login
-> GET /identity/auth/session
-> GET /companies/available
-> resolver empresa activa
-> auto-selección si hay una sola empresa
-> selector si hay varias
-> /no-companies si no hay empresas
-> X-Company-Id en rutas tenant-scoped
```
## Login
El login usa:
```http
POST /identity/auth/login
```
Tras login correcto:
- se guardan `accessToken` y `refreshToken`
- se limpia cualquier `activeCompanyId` previo
- se carga `GET /companies/available`
- se resuelve el redirect autenticado según companies disponibles
## Session restore
Si existe `accessToken` persistido:
1. se llama `GET /identity/auth/session`
2. se hidrata la sesión autenticada
3. se llama `GET /companies/available`
4. se resuelve `activeCompanyId` persistido o auto-selección
Si la restauración falla:
- se limpia la sesión
- se limpia `activeCompanyId`
## Selección de empresa
### Cero empresas
Si `companies.length === 0`:
- se muestra `/no-companies`
- no existe `activeCompanyId`
- no se envía `X-Company-Id`
- sigue disponible el logout
### Una empresa
Si `companies.length === 1`:
- se selecciona automáticamente
- se guarda `activeCompanyId`
- no se muestra `/company-selection`
- se continúa al área privada
### Varias empresas
Si `companies.length > 1`:
- se muestra `/company-selection`
- no existe `activeCompanyId` hasta elegir
- el usuario selecciona company
- se guarda `activeCompanyId`
- se continúa al área privada
### `activeCompanyId` persistido ya no disponible
Si el `activeCompanyId` persistido ya no aparece en `GET /companies/available`:
- con `0` companies -> `/no-companies`
- con `1` company -> auto-selección de la disponible
- con varias companies -> `/company-selection`
## `GET /companies/available`
Endpoint actual:
```http
GET /companies/available
Authorization: Bearer <access_token>
```
No requiere:
- `X-Company-Id`
No usa:
- `requireIdentityTenant(params)`
Devuelve:
```ts
export type AvailableCompaniesResponseDTO = {
companies: AvailableCompanyDTO[];
};
```
## App company switcher
Componente documentado:
```txt
apps/web/src/layout/app-company-switcher.tsx
```
Propósito:
- cambiar de empresa activa una vez iniciada la sesión
- mostrar la empresa activa actual dentro del layout autenticado
Reglas:
- consume `availableCompanies` y `activeCompany` desde `useIdentityAuthSession()`
- usa `changeActiveCompany()` o `selectActiveCompany()` del contexto, no requests directas
- no llama directamente a `GET /companies/available` si el contexto ya está hidratado
- no maneja tokens
- no usa `@erp/auth/client`
- no usa `companySlug` para autenticación
- no modifica el access token
- al cambiar empresa, las siguientes requests tenant-scoped deben salir con el nuevo `X-Company-Id`
Comportamiento esperado:
- `0` empresas: no rompe; render deshabilitado o equivalente
- `1` empresa: muestra la empresa activa; no hace falta selector operativo
- `N` empresas: permite cambiar entre las permitidas y marca la activa
## Headers
### No deben llevar `X-Company-Id`
```txt
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
GET /companies/available
```
### Sí deben llevar `X-Company-Id`
Rutas tenant-scoped como:
- `/customers`
- `/catalogs/*`
- `/customer-invoices/*`
- `/factuges`
siempre que exista `activeCompanyId`.
## Refresh automático sobre `401`
El frontend implementa refresh automático global sobre Axios.
Comportamiento:
1. una request autenticada recibe `401`
2. si existe `refreshToken`, se llama `POST /identity/auth/refresh`
3. se guardan los nuevos tokens
4. se reintenta una única vez la request original
5. si refresh falla, se limpia la sesión y se redirige a `/login`
## Reglas implementadas
- retry único por request con `_retry`
- refresh concurrente único con `refreshPromise` compartida
- varias requests `401` esperan el mismo refresh
- login `401` no dispara refresh
- refresh `401` no intenta refrescarse a sí mismo
- `/identity/auth/refresh` no lleva `X-Company-Id`
- la request tenant-scoped reintentada conserva `X-Company-Id`
## Endpoints excluidos de tenant header y refresh
```txt
/identity/auth/login
/identity/auth/refresh
/identity/auth/logout
/identity/auth/session
/companies/available
```
## Pruebas manuales recomendadas
1. Login con usuario sin empresas: debe ir a `/no-companies`.
2. Login con usuario con una empresa: debe auto-seleccionarse y entrar al área privada.
3. Login con usuario con varias empresas: debe mostrarse `/company-selection`.
4. Cambio de empresa desde `app-company-switcher`: la request tenant-scoped siguiente debe llevar el nuevo `X-Company-Id`.
5. Access token expirado con refresh válido: debe ejecutarse refresh + retry único.
6. Refresh token inválido: debe limpiarse la sesión y redirigir a `/login`.
7. Endpoints auth y `GET /companies/available`: no deben llevar `X-Company-Id`.
8. Rutas tenant-scoped: deben llevar `X-Company-Id` cuando exista `activeCompanyId`.

View File

@ -1,194 +0,0 @@
# Diseño de cuadros de diálogo de formulario
## Estado
Este documento es la fuente de verdad para diseñar, implementar y revisar cuadros de diálogo que recogen o modifican datos en el frontend del ERP.
Todo diálogo nuevo de creación, edición o configuración debe seguir esta guía, salvo que una necesidad funcional justifique y documente una excepción.
## Referencia canónica
La referencia productiva es el diálogo para crear una proforma recurrente desde una proforma:
- componente: `modules/customer-invoices/src/web/proformas/detail/ui/components/create-recurring-proforma-from-proforma-dialog.tsx`;
- controller: `modules/customer-invoices/src/web/proformas/detail/controllers/use-create-recurring-proforma-from-proforma-dialog-controller.ts`;
- prueba de renderizado: `modules/customer-invoices/src/web/proformas/detail/ui/components/create-recurring-proforma-from-proforma-dialog.test.tsx`.
La guía no obliga a reutilizar ese componente. Obliga a mantener su jerarquía visual, separación de responsabilidades y comportamiento accesible.
## Cuándo aplicar este patrón
Usar `Dialog` para una tarea breve y focalizada que:
- se inicia desde la pantalla actual;
- requiere pocos datos para completar una única acción;
- no necesita una ruta propia ni navegación lateral;
- permite volver al contexto sin perder orientación.
No usarlo para:
- confirmaciones o acciones destructivas sin formulario: usar `AlertDialog`;
- ediciones documentales complejas, varios pasos o trabajo prolongado: usar una página de edición;
- vistas de solo lectura: usar un diálogo de consulta o el patrón existente del área.
## Estructura visual obligatoria
Un diálogo de formulario se organiza de arriba abajo en cuatro zonas:
1. Cabecera diferenciada.
2. Campo de identidad destacado cuando exista, normalmente el nombre.
3. Bloques de información agrupados por intención.
4. Pie de acciones separado del contenido.
```tsx
<DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-2xl">
<DialogHeader className="border-b bg-muted/40 px-6 py-5 sm:px-7">
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="max-w-lg">{description}</DialogDescription>
</DialogHeader>
<form className="space-y-6 px-6 py-5 sm:px-7">
<IdentityField />
<InformationSection />
<ConfigurationSection />
</form>
<DialogFooter className="border-t bg-muted/30 px-6 py-4 sm:px-7">
<CancelAction />
<SubmitAction />
</DialogFooter>
</DialogContent>
```
No añadir una zona si no aporta información o una acción real. La estructura debe mejorar la lectura, no convertir un formulario pequeño en una pantalla artificialmente compleja.
## 1. Cabecera diferenciada
La cabecera comunica qué se va a hacer antes de pedir datos.
- Incluir título directo, orientado a la acción: `Crear plazo de pago`, `Modificar método de pago`.
- Incluir una descripción breve que explique el resultado o el contexto de origen.
- Separarla visualmente del formulario mediante borde inferior y fondo sutil del tema (`bg-muted/40`).
- Mantener los márgenes internos alineados con contenido y pie.
- No colocar acciones de formulario en la cabecera; el cierre estándar del diálogo se mantiene disponible.
## 2. Campo de identidad destacado
Cuando el recurso tenga un nombre, código o referencia que ayude a reconocerlo, ese campo va inmediatamente después de la cabecera.
- Usar el nombre como primer campo si es obligatorio para crear o identificar el recurso.
- Dar al control una presencia moderada (`h-11` y fondo de formulario) sin crear un segundo título.
- Mantener el error de validación junto al campo.
- No destacar campos técnicos, identificadores internos ni valores que el usuario no pueda modificar.
Si el diálogo no tiene un campo de identidad relevante, el primer bloque de información pasa a ocupar esa posición.
## 3. Bloques de información
Agrupar campos que responden a la misma pregunta del usuario. Por ejemplo: periodo de vigencia, recurrencia, condiciones comerciales o destinatario.
Cada bloque debe:
- usar un `section` con `aria-labelledby`;
- tener un título visible y conciso;
- mostrar un icono solo cuando ayude a identificar el tipo de información;
- usar borde, radio y fondo sutil para delimitarlo sin competir con la acción principal;
- contener entre uno y seis controles relacionados por norma general.
Patrón base:
```tsx
<section aria-labelledby={sectionId} className="rounded-xl border bg-muted/20 p-4 sm:p-5">
<div className="mb-4 flex items-center gap-3">
<span className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
<SectionIcon aria-hidden className="size-4" />
</span>
<h3 className="font-medium text-sm" id={sectionId}>{sectionTitle}</h3>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<Field />
<Field />
</div>
</section>
```
Reglas de composición:
- En escritorio, los campos de igual importancia pueden compartir dos columnas.
- En móvil, las columnas deben apilarse de forma natural. Usar `sm:grid-cols-2`, nunca una anchura fija.
- Añadir `min-w-0` a las columnas cuando puedan contener textos localizados, selectores o controles flexibles.
- Los selectores e inputs ocupan todo el ancho de su columna.
- Un campo condicional se muestra dentro del bloque al que pertenece y se separa con borde superior si necesita distinguirse del resto.
- Los interruptores y checkboxes que afectan a otro campo deben vivir junto a ese campo, no en una fila independiente sin contexto.
## 4. Pie de acciones
El pie mantiene las decisiones finales fuera de los datos.
- Separarlo mediante borde superior y fondo sutil (`bg-muted/30`).
- Colocar `Cancelar` como acción secundaria y la acción de envío como botón principal.
- Durante el envío, deshabilitar ambos botones y todos los controles editables.
- El botón principal debe reflejar el estado de progreso con texto o icono accesible.
- Asociar el botón de envío al `form` mediante su atributo `form` cuando el pie quede fuera del elemento `form`.
## Accesibilidad, validación e i18n
- Usar `DialogTitle` y `DialogDescription` siempre que el diálogo tenga contenido operativo.
- Asociar cada `Label` a un control mediante `htmlFor` e `id`.
- Los títulos de bloque usan `section` y `aria-labelledby`.
- Mantener los errores junto al control que los genera; no depender únicamente de avisos globales.
- Todo texto visible procede de i18n y debe existir en `es` y `en`.
- Los iconos decorativos usan `aria-hidden`; nunca sustituyen una etiqueta textual.
- No bloquear el cierre por teclado salvo mientras exista una operación de envío en curso.
## Responsabilidades del código
El componente de UI compone el diálogo y recibe estado ya preparado. No debe contener navegación, peticiones HTTP ni reglas de negocio.
El controller del diálogo concentra:
- valores iniciales y reinicio del formulario;
- validación de formulario;
- llamada al hook de mutación;
- estado de envío;
- cierre, notificaciones y navegación tras un resultado correcto.
Las operaciones remotas permanecen en hooks de TanStack Query. Los DTOs de petición siguen siendo la fuente de verdad del formulario y de la validación.
## Pruebas mínimas
Cada diálogo nuevo o modificado debe cubrir, como mínimo:
- título, descripción y campo de identidad cuando exista;
- campos obligatorios y campos condicionales;
- acciones de cancelar y enviar;
- estado de envío;
- traducciones reales o una prueba que compruebe que las claves usadas existen en el catálogo;
- resultado correcto y error de la mutación en el controller.
No usar un diccionario de traducciones manual en el test si el objetivo es verificar que el catálogo productivo contiene las claves consumidas por el componente.
## Lista de comprobación
Antes de cerrar un cambio, confirmar:
- La tarea cabe realmente en un diálogo.
- La cabecera explica la acción y está diferenciada del formulario.
- El nombre o campo de identidad aparece antes de los bloques de configuración.
- Cada bloque agrupa una única intención comprensible.
- El layout se apila correctamente en móvil.
- Los textos, estados disabled, errores y carga son accesibles.
- Existen traducciones en `es` y `en`.
- Las pruebas y la validación de formato se han ejecutado desde Linux.
## Excepciones
Una excepción debe justificarse en la revisión del cambio indicando:
- qué requisito funcional impide usar esta estructura;
- qué patrón alternativo se emplea;
- cómo se mantiene la accesibilidad y el comportamiento responsive;
- qué pruebas cubren la variante.
La preferencia visual local, copiar un diálogo antiguo o ahorrar un bloque de composición no son motivos válidos para omitir esta guía.

View File

@ -1,166 +0,0 @@
# Diseño de pantallas de listado
## Estado
Este documento es la fuente de verdad para diseñar, implementar y revisar pantallas de listado del frontend del ERP.
Toda pantalla de listado nueva debe seguir esta guía salvo que exista una excepción funcional documentada antes de implementar el cambio. Las pantallas existentes se alinearán de forma incremental cuando se modifiquen.
## Referencia canónica
La referencia productiva es el listado de proformas:
- página: `modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx`;
- controller de página: `modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas-page.controller.ts`;
- controller de datos: `modules/customer-invoices/src/web/proformas/list/controllers/use-list-proformas.controller.ts`;
- vista de shell: `modules/customer-invoices/src/web/proformas/list/ui/pages/scoped-list-proformas-page-view.tsx`.
No se exige igualdad textual ni reutilización forzada de componentes de dominio. Se exige conservar la misma separación de responsabilidades y la misma jerarquía visual.
## Estructura visual obligatoria
Una página de listado se compone, por defecto, así:
```tsx
<RdxPageContainer width="full">
<RdxPageStack>
<RdxPageHeader
title={title}
description={description}
actions={<RdxPageActions primary={primaryAction} />}
/>
{refreshError ? <ErrorAlert {...refreshError} /> : null}
<DataTable
topContent={toolbarAndFilters}
{...tableProps}
/>
{isEmpty ? <EmptyState /> : null}
</RdxPageStack>
</RdxPageContainer>
```
Reglas:
- `RdxPageHeader` vive dentro de `RdxPageStack`, no como un `header` HTML paralelo.
- Las acciones principales se presentan en el header mediante `RdxPageActions` o el mecanismo equivalente del shell existente.
- La búsqueda, filtros y controles de tabla viven en `DataTable.topContent` y preceden a la tabla.
- La tabla, el estado vacío y el error de refresco permanecen dentro del mismo stack para evitar saltos de layout.
- Debe respetarse el spacing del shell; no añadir wrappers de anchura, márgenes o stacks sin una necesidad de contenido demostrable.
- El CTA de creación se mantiene visible cuando hay datos, carga, vacío o un error de refresco. Sólo desaparece con un error inicial bloqueante.
La pantalla puede usar una vista de composición específica del dominio, como `ScopedListProformasPageView`, cuando encapsule exclusivamente el shell anterior. No debe duplicar ese JSX en cada lista.
## Ownership del controller de página
Cada listado debe tener un controller de presentación cuando su página coordine consulta, URL, filtros, paginación o navegación. Su nombre sigue el patrón:
```text
use-list-<recurso>-page-controller.ts
useList<Resource>PageController
```
El controller de página concentra:
- lectura y escritura de `searchParams` relevantes;
- estado de búsqueda y su debounce, si existe;
- filtros, orden y paginación server-side;
- composición del hook de datos existente de TanStack Query;
- adaptación mínima de resultados para la tabla;
- view state de carga, error inicial bloqueante y error durante refresco;
- callbacks de reintento, creación y navegación a detalle;
- inputs y handlers mínimos que necesita la página.
La página queda declarativa: define columnas y compone el shell, el toolbar y la tabla con los datos y callbacks del controller. No debe tener `useSearchParams`, llamar directamente al hook de listado ni construir rutas de navegación desde eventos de la tabla.
El controller no contiene reglas de dominio, cálculos de negocio, fetch manual, lógica de backend ni abstracciones genéricas de listados. Reutiliza los query hooks y query keys del dominio.
## Consulta, filtros y paginación
- Mantener la consulta remota en TanStack Query.
- Mantener paginación server-side mediante `manualPagination`, `pageIndex`, `pageSize`, `totalItems` y los callbacks correspondientes.
- Cambiar búsqueda, filtro, orden o tamaño de página actualiza la URL con la misma semántica del recurso y reinicia la página cuando corresponda.
- No duplicar datos remotos en estado local.
- No añadir filtros, columnas ni acciones por motivos visuales.
- Los filtros segmentados reutilizan el patrón visual de `ProformaStatusSegmentedFilter`, pero son componentes de dominio distintos si sus estados, traducciones o datos difieren.
- Nunca mostrar contadores globales de estado que el endpoint no proporciona. No se calculan a partir de una página parcial de resultados.
## Tabla y navegación
- Conservar columnas, orden, formato y acciones ya productivas salvo que la tarea cambie explícitamente su funcionalidad.
- El enlace de una celda y el click de fila deben llevar al mismo detalle cuando ambos existan.
- `DataTable.onRowClick` recibe el objeto de fila, no un identificador. La página lo adapta explícitamente:
```tsx
onRowClick={(row) => controller.openDetailPage(row.id)}
```
- No pasar un callback que espera `id` directamente a `DataTable.onRowClick`; produciría rutas como `[object Object]`.
- La navegación de detalle y creación se define en el controller de página. La página sólo conecta el evento con el callback.
- No introducir N+1 para resolver nombres o datos de columnas. Si un dato no está en el contrato del listado, debe resolverse mediante una evolución explícita y justificada del contrato.
## Estados de interfaz
La lista distingue tres situaciones:
| Situación | Comportamiento |
| --- | --- |
| Carga inicial | La tabla recibe su estado `loading`; se mantiene el shell de página. |
| Error inicial sin datos utilizables | Mostrar `PageErrorState` dentro de `RdxPageContainer`, con reintento. |
| Error al refrescar con datos utilizables | Mantener tabla y filtros; mostrar `ErrorAlert` inline en el stack. |
Además:
- el empty state se muestra cuando la respuesta utilizable tiene cero elementos;
- no crear un segundo sistema de skeleton, alertas o errores si ya existen `DataTable`, `PageErrorState` y `ErrorAlert`;
- las acciones deben reflejar loading y disabled cuando el contrato del componente lo permita;
- no ocultar acciones deshabilitadas si la UX requiere que sigan siendo visibles.
## Accesibilidad e i18n
- Los filtros tienen etiqueta accesible y controles navegables por teclado.
- El click de fila debe conservar activación con `Enter` y espacio provista por `DataTable`.
- No insertar textos visibles directamente: añadir y mantener traducciones en `es` y `en` cuando hagan falta.
- Mantener `tabular-nums` en valores monetarios y fechas numéricas cuando el patrón de tabla lo use.
## Pruebas mínimas
Al crear o refactorizar un listado, adaptar o añadir pruebas funcionales de página que cubran:
- presencia y navegación del CTA de creación;
- búsqueda;
- filtro de estado u otros filtros ya existentes;
- paginación server-side;
- carga inicial, error bloqueante, error de refresco y vacío;
- renderizado de filas y formatos sensibles, como fechas sin zona horaria;
- navegación a detalle desde fila;
- que el callback de `DataTable.onRowClick` recibe una fila y usa su `id`.
Los tests no deben depender de detalles internos irrelevantes del controller ni sustituir contratos de tabla por tipos incorrectos.
## Validación obligatoria
Antes de cerrar un cambio de listado, ejecutar en Linux y desde `/home/rodax/Documentos/uecko-erp`:
```bash
pnpm test:frontend <tests-dirigidos>
pnpm --filter <workspace> typecheck
pnpm --filter <workspace> lint
git diff --check
git status --short
```
Si typecheck o lint fallan por problemas preexistentes ajenos, ejecutar también Biome sobre los archivos modificados y comunicar el fallo global sin atribuirlo al cambio.
## Excepciones
Una excepción requiere dejar constancia en la revisión o en documentación del área con:
- el requisito funcional que impide seguir esta guía;
- el impacto visual o arquitectónico;
- el patrón alternativo elegido;
- las pruebas que lo cubren.
No son excepciones válidas la preferencia local, la rapidez de implementación, copiar una pantalla antigua o evitar extraer lógica de una página monolítica.

View File

@ -1,274 +0,0 @@
# Auditoría De Layouts De Editores
Fecha de auditoría: 18 de agosto de 2026.
## Objetivo
Inventariar los editores reales del frontend del ERP y clasificarlos respecto a la norma visual vigente de edición focalizada.
Esta auditoría:
- inventaría rutas, páginas y dialogs de edición activos;
- clasifica cada caso por tipo;
- evalúa el layout actual frente a la norma;
- detecta desviaciones y excepciones justificadas;
- propone una secuencia pequeña de migración.
Esta auditoría no migra masivamente las pantallas.
## Norma visual de referencia
SSOT vigente:
- `packages/rdx-ui/docs/page-layouts.md`
- `packages/rdx-ui/docs/contributing-ui.md`
Resumen operativo:
- los editores de página completa deben usar por defecto shell `app-fullscreen`;
- la navegación mínima sale por `PageFormHeader` o un header de dominio equivalente;
- el contenido del editor se centra mediante `RdxEditorPageLayout`;
- `complexity="simple"` aplica a formularios focalizados centrados;
- `complexity="complex"` aplica a documentos o editores con `main + aside`, bloques especializados y footer sticky;
- los dialogs e inline editors no deben migrarse automáticamente a fullscreen.
## Método de investigación
Búsquedas y fuentes revisadas:
- rutas y layouts en `apps/web/src/routes/app-routes.tsx` y `modules/*/src/web/*routes.tsx`
- contratos de layout en `modules/core/src/web/lib/modules/module-client.interface.ts`
- primitivas Rdx en:
- `packages/rdx-ui/src/components/editor-page/rdx-editor-page-layout.tsx`
- `packages/rdx-ui/src/components/editor-page/rdx-editor-sticky-footer.tsx`
- `modules/core/src/web/components/form/form-actions/page-form-header.tsx`
- `apps/web/src/layout/app-fullscreen-layout.tsx`
- `apps/web/src/layout/app-sidebar-layout.tsx`
- módulos revisados:
- `identity`
- `customers`
- `customer-invoices`
- `companies`
- `catalogs`
- `document-series`
- `supplier`
- búsquedas textuales sobre:
- `layout: "app-fullscreen"`
- `layout: "app-sidebar"`
- `RdxEditorPageLayout`
- `PageFormHeader`
- `RdxEditorStickyFooter`
- `create`
- `update`
- `edit`
- `Dialog`
## Inventario
| Module | Feature | Route | Type | Current layout | Rdx editor layout | Compliance | Risk | Priority | Recommended action | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `identity` | Account self-service | `/account` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como referencia de editor simple focalizado. | `modules/identity/src/web/account-routes.tsx`, `modules/identity/src/web/account/ui/pages/account-page.tsx` |
| `identity` | Change password dialog | Sin ruta propia. Dialog dentro de `/account` | `DIALOG` | Host `app-fullscreen` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como dialog de edición contenida. | `modules/identity/src/web/account/ui/pages/account-page.tsx` |
| `customers` | Create customer | `/customers/create` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `LOW` | `-` | Ninguna. Referencia vigente de editor simple. | `modules/customers/src/web/customer-routes.tsx`, `modules/customers/src/web/create/ui/pages/customer-create-page.tsx` |
| `customers` | Update customer | `/customers/:id/edit` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `LOW` | `-` | Ninguna. Referencia vigente de editor simple. | `modules/customers/src/web/customer-routes.tsx`, `modules/customers/src/web/update/ui/pages/customer-update-page.tsx` |
| `customers` | Quick create customer | Sin ruta propia. Dialog reutilizado desde proformas | `DIALOG` | Host `app-fullscreen` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantenerlo como creación rápida embebida. | `modules/customers/src/common/features/quick-create-customer/ui/dialogs/quick-create-customer-dialog.tsx` |
| `companies` | Company details settings | `/settings/company/details` | `PAGE_SIMPLE` | `app-sidebar` | No usa `RdxEditorPageLayout` | `EXCEPTION_JUSTIFIED` | `MEDIUM` | `P3` | Mantener en settings por ahora. Revaluar solo si se define una variante focalizada específica para formularios de settings. | `modules/companies/src/web/company-routes.tsx`, `modules/companies/src/web/settings/company-details/ui/pages/company-details-settings-page.tsx` |
| `customer-invoices` | Verifactu settings | `/settings/billing/verifactu` | `PAGE_SIMPLE` | `app-sidebar` | No usa `RdxEditorPageLayout` | `EXCEPTION_JUSTIFIED` | `MEDIUM` | `P3` | Mantener en settings por ahora. Revaluar junto con una política específica de formularios largos dentro de settings. | `modules/customer-invoices/src/web/customer-invoice-routes.tsx`, `modules/customer-invoices/src/web/settings/verifactu/ui/pages/company-verifactu-settings-page.tsx` |
| `customer-invoices` | Create proforma | `/proformas/create` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `MEDIUM` | `-` | Ninguna. Mantener como bootstrap inicial de proforma hacia el editor documental completo. | `modules/customer-invoices/src/web/customer-invoice-routes.tsx`, `modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx`, `modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx` |
| `customer-invoices` | Update proforma | `/proformas/:id/edit` | `PAGE_COMPLEX` | `app-fullscreen` | `complex` | `COMPLIANT` | `HIGH` | `-` | Ninguna. Mantener como referencia vigente de editor complejo. | `modules/customer-invoices/src/web/customer-invoice-routes.tsx`, `modules/customer-invoices/src/web/proformas/update/ui/pages/proforma-update-page.tsx`, `modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx` |
| `customer-invoices` | Change proforma status | Sin ruta propia. Dialog usado desde listado y detalle | `DIALOG` | Host mixto (`app-sidebar` o `app-fullscreen`) | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Es una edición contenida por dialog, no candidata a fullscreen. | `modules/customer-invoices/src/web/proformas/change-status/ui/change-status-dialog.tsx`, `modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx`, `modules/customer-invoices/src/web/proformas/detail/ui/pages/proforma-detail-page.tsx` |
| `catalogs` | Payment methods create/update | Host: `/settings/general/payment-methods` | `DIALOG` | Host `app-sidebar` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como edición contenida en dialog dentro de settings. | `modules/catalogs/src/web/catalogs-routes.tsx`, `modules/catalogs/src/web/settings/payment-methods/ui/pages/payment-methods-settings-page.tsx`, `modules/catalogs/src/web/settings/payment-methods/ui/components/payment-method-form-dialog.tsx` |
| `catalogs` | Payment terms create/update | Host: `/settings/general/payment-terms` | `DIALOG` | Host `app-sidebar` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como dialog aunque su contenido interno sea más rico. | `modules/catalogs/src/web/catalogs-routes.tsx`, `modules/catalogs/src/web/settings/payment-terms/ui/pages/payment-terms-settings-page.tsx`, `modules/catalogs/src/web/settings/payment-terms/ui/components/payment-term-form-dialog.tsx` |
| `catalogs` | Tax regimes create/update | Host: `/settings/general/tax-regimes` | `DIALOG` | Host `app-sidebar` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como dialog de edición contenida. | `modules/catalogs/src/web/catalogs-routes.tsx`, `modules/catalogs/src/web/settings/tax-regimes/ui/pages/tax-regimes-settings-page.tsx`, `modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-form-dialog.tsx` |
| `customer-invoices` | Issued invoices create/edit references | Referencias a `/issued-invoices/create` y `/customer-invoices/:id/edit`, sin ruta activa | `UNCLEAR` | Sin ruta activa | No aplica | `UNKNOWN` | `MEDIUM` | `P2` | Aclarar si esas rutas deben existir o limpiar referencias muertas antes de cualquier trabajo visual. | `modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx`, `modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/issued-invoices-grid.tsx`, `modules/customer-invoices/src/web/customer-invoice-routes.tsx` |
| `document-series` | Frontend editor activo | No detectado | `LEGACY_OR_UNUSED` | Sin route frontend | No aplica | `UNKNOWN` | `LOW` | `P3` | Confirmar si el módulo tendrá UI activa antes de cualquier migración de layout. | Sin `src/web/*routes.tsx` ni páginas frontend activas en `modules/document-series` |
| `supplier` | Frontend editor activo | No detectado | `LEGACY_OR_UNUSED` | Sin route frontend | No aplica | `UNKNOWN` | `LOW` | `P3` | Resolver primero si el módulo se reactiva o se limpia; no priorizar migración visual. | Sin `src/web/*routes.tsx`; solo `modules/supplier/src/web/shared/*` |
## Editores conformes
Conformes respecto a la norma vigente:
- `customers/create`
- `customers/update`
- `/account`
- `proformas/:id/edit`
Motivos comunes observados:
- ruta bajo `app-fullscreen`;
- navegación mínima por `PageFormHeader` o header de dominio equivalente;
- uso explícito de `RdxEditorPageLayout`;
- geometría coherente con `simple` o `complex` según caso;
- acciones de salida/guardado visibles y no delegadas al sidebar.
## Desviaciones
### 1. `proformas/create`
HECHO OBSERVADO
- la ruta usa `app-fullscreen`;
- la página usa header de edición y `RdxEditorPageLayout complexity="simple"`;
- el flujo de alta actual solo captura cabecera inicial y deriva después al editor documental completo;
- no existe todavía editor de líneas, `aside`, totales sticky ni footer persistente equivalentes a `proformas/:id/edit`.
IMPACTO
- el create queda alineado con el patrón focalizado simple sin fingir que ya es el editor documental completo;
- la diferencia visual restante frente a `update` responde a una diferencia funcional real y no a drift accidental de layout.
DECISIÓN
- `PAGE_SIMPLE`;
- `COMPLIANT`.
### 2. Referencias a create/edit de issued invoices sin ruta activa
HECHO OBSERVADO
- hay referencias a `/issued-invoices/create` y a `/customer-invoices/:id/edit`;
- `customer-invoice-routes.tsx` no declara esas rutas activas.
IMPACTO
- el inventario de editores queda ambiguo en esa área;
- puede existir deuda de navegación muerta o una funcionalidad incompleta;
- no conviene planificar migración visual ahí hasta aclarar si hay editor real.
RECOMENDACIÓN
- confirmar si esas rutas deben implementarse;
- si no, limpiar referencias muertas antes de meterlas en cualquier backlog de layouts.
## Excepciones justificadas
### Company details settings
HECHO OBSERVADO
- es un formulario de página completa;
- vive bajo `/settings/company/details`;
- depende del shell de settings con `app-sidebar`.
INFERENCIA
- la navegación lateral de settings forma parte funcional de la tarea y hoy actúa como mecanismo primario de cambio de subsección.
DECISIÓN DE AUDITORÍA
- `EXCEPTION_JUSTIFIED` por ahora;
- no priorizar fullscreen salvo que el sistema de settings evolucione con una variante focalizada explícita.
### Verifactu settings
HECHO OBSERVADO
- es un formulario de configuración largo;
- vive bajo `/settings/billing/verifactu`;
- comparte patrón de settings con shell lateral.
INFERENCIA
- igual que en `company/details`, la tarea sigue integrada en navegación de settings y no en un editor aislado.
DECISIÓN DE AUDITORÍA
- `EXCEPTION_JUSTIFIED` por ahora;
- revisar sólo si se aborda una política transversal de formularios largos dentro de settings.
## Dialogs e inline editors
Dialogs correctamente clasificados y no candidatos automáticos a fullscreen:
- `QuickCreateCustomerDialog`
- `PaymentMethodFormDialog`
- `PaymentTermFormDialog`
- `TaxRegimeFormDialog`
- dialog de cambio de contraseña de `/account`
- `ChangeProformaStatusDialog`
No se detectaron rutas independientes que deban reclasificarse desde `DIALOG` o `INLINE` a `PAGE_*`.
No se detectaron inline editors de entidad de primer nivel con route propia. Sí existen bloques especializados internos, como el editor de líneas de proformas, pero forman parte de un `PAGE_COMPLEX` y no deben inventariarse como pantalla independiente.
## Incoherencias create/update
Agregados revisados:
- `customers`
- `create` y `update` comparten patrón y están alineados.
- `proformas`
- `create` usa `RdxEditorPageLayout complexity="simple"` porque hoy es un bootstrap inicial;
- `update` usa `RdxEditorPageLayout complexity="complex"` porque ya es el editor documental completo con `main`, bloques especializados y footer sticky.
Conclusión:
- la diferencia create/update restante es funcional legítima: `create` todavía no edita el documento completo y `update` sí.
## Riesgos de migración
Riesgos bajos:
- `customers/create`
- `customers/update`
- `/account`
Riesgos medios:
- `proformas/create` por convivencia con quick-create/select customer dialogs y porque el flujo actual parece de bootstrap inicial, no de edición documental completa;
- `company/details` y `verifactu` si se intentara moverlos fuera del shell settings.
Riesgos altos:
- `proformas/:id/edit`, aunque hoy ya cumple. Cualquier cambio allí impacta:
- scroll complejo
- footer sticky
- totales embebidos
- `aside`
- dialogs anidados
## Plan incremental
### Fase A
- completada: migración de `proformas/create`
Motivo:
- era el único `PAGE_*` activo no plenamente alineado con el patrón Rdx;
- ya usaba `app-fullscreen`, así que el salto era incremental;
- permite distinguir mejor entre bootstrap inicial y editor documental completo del agregado Proforma.
### Fase B
- revisar si `company/details` y `verifactu` deben seguir como excepción de settings o si merece una variante focalizada específica para formularios largos dentro de settings
Motivo:
- ahora mismo la excepción parece razonable;
- no conviene forzar fullscreen sin una decisión explícita sobre el sistema de settings.
### Fase C
- aclarar `issued-invoices` create/edit
Motivo:
- primero hay que determinar si existe editor real o solo referencias muertas.
### Fase D
- hacer una auditoría UX más fina de dialogs ricos, empezando por `payment-terms`
Motivo:
- no es un incumplimiento de fullscreen;
- sí es el dialog más complejo de los auditados y puede requerir reglas específicas de consistencia interna.
## No cambios
Durante esta auditoría:
- no se modificaron rutas ni layouts runtime de `customers`, `companies`, `customer-invoices`, `catalogs`, `identity`, `document-series` o `supplier`;
- no se tocaron hooks, controllers, formularios, React Query ni backend de los editores auditados;
- el único cambio de código del turno es este documento operativo de auditoría.

View File

@ -0,0 +1,133 @@
# Arquitectura frontend
> **Propósito:** explicar cómo está construido el frontend. Este documento, junto con [UI system](./ui-system.md) y [UI patterns](./ui-patterns.md), forma la fuente de verdad (SSOT) de frontend/UI.
## 1. Stack
El frontend usa React 19, TypeScript estricto, React Router, TanStack Query, React Hook Form, Axios, Tailwind CSS v4, Shadcn/UI y `@repo/rdx-ui`.
## 2. Arquitectura por capas
El flujo habitual de una feature es:
```text
API → DTO → adapter → Entity → query hook → controller → page controller → Page → UI
```
- `shared/api`: transporte y contratos HTTP.
- `shared/adapters`: transforma DTOs en entidades de frontend.
- `shared/entities`: modelos consumidos por la aplicación.
- `shared/hooks`: queries, mutations y query keys de TanStack Query.
- `controllers`: orquestación funcional, formularios, navegación y efectos.
- `page controllers`: componen el estado completo de una pantalla.
- `Page`: conecta controller y componentes; debe tener poco estado propio.
- `ui`: renderiza props preparadas y emite eventos.
La UI no debe contener reglas de dominio, fetch manual ni traducciones entre DTO y entidad. Los controllers no deben convertirse en servicios de dominio.
## 3. Módulos frontend
Cada módulo publica sus rutas y su cliente desde `modules/<module>/src/web`. Las features se organizan por caso de uso (`list`, `create`, `update`, `detail`) y comparten transporte, entidades y hooks mediante `shared` cuando existe reutilización real.
Las dependencias deben apuntar hacia contratos compartidos, no hacia páginas concretas. Un componente de dominio puede envolver una foundation visual si aporta defaults o vocabulario propios, pero no debe recrear el layout de página.
## 4. Routing y layouts
Las rutas declaran su shell en `handle.layout`:
- `auth`: login y recuperación, bajo `AuthLayout`.
- `app-sidebar`: dashboard, listados, settings y navegación general.
- `app-fullscreen`: tareas focalizadas de create/update/detail y Account cuando la ruta así lo declara.
`apps/web/src/routes/app-routes.tsx` agrupa las rutas por ese valor y las monta bajo el layout correspondiente. `protected` y `requireCompany` expresan guards independientes del shell. La elección debe seguir las rutas productivas del área; no se infiere únicamente por el nombre de la pantalla.
## 5. Shells de aplicación
```text
Application shell
├── AppSidebarLayout
│ ├── AppSidebar
│ ├── AppTopbar
│ └── AppMain
├── AppFullscreenLayout
│ └── AppMain
└── AuthLayout
```
- `AppSidebarLayout` proporciona navegación y topbar.
- `AppFullscreenLayout` ocupa el viewport para una tarea focalizada.
- `AuthLayout` centra las pantallas públicas de identidad.
- `AppMain` entrega el espacio disponible. No es propietario del scroll de una página.
Dentro de `AppMain`, una pantalla usa `RdxPageFrame`; el sistema visual y sus reglas se documentan en [UI system](./ui-system.md).
## 6. Identity y sesión
El runtime actual se compone en `apps/web/src/app.tsx`:
```text
DataSourceProvider
└── IdentityAuthSessionProvider
└── router
```
El login usa `POST /identity/auth/login`. Tras autenticar:
1. persiste `accessToken` y `refreshToken`;
2. elimina cualquier empresa activa anterior;
3. obtiene la sesión con `GET /identity/auth/session`;
4. carga `GET /companies/available`;
5. resuelve la empresa activa y el destino autenticado.
Al restaurar una sesión, el provider valida el token, hidrata identidad y empresas y limpia ambos estados si la restauración falla. El frontend nuevo usa `@erp/identity/client`; no debe introducir dependencias nuevas sobre el runtime legacy de auth.
## 7. Selección de empresa
- Sin empresas: navega a `/no-companies`; no existe `activeCompanyId` y el logout permanece disponible.
- Una empresa: se selecciona automáticamente.
- Varias empresas: navega a `/company-selection` hasta que el usuario elige.
- Empresa persistida no disponible: vuelve a aplicar las tres reglas anteriores.
El switcher autenticado consume `availableCompanies`, `activeCompany` y `changeActiveCompany()` desde `useIdentityAuthSession()`. No obtiene tokens ni repite la consulta directamente.
## 8. Contexto tenant y `X-Company-Id`
Las requests tenant-scoped incluyen `X-Company-Id` cuando existe una empresa activa. No lo incluyen:
```text
POST /identity/auth/login
POST /identity/auth/refresh
POST /identity/auth/logout
GET /identity/auth/session
GET /companies/available
```
Los guards de compañía resuelven la navegación; los componentes visuales no deben inspeccionar headers HTTP ni decidir tenancy.
## 9. Refresh de Axios
Ante un `401` autenticado, el interceptor:
1. inicia `POST /identity/auth/refresh` si existe refresh token;
2. comparte una única `refreshPromise` entre requests concurrentes;
3. persiste los tokens renovados;
4. reintenta una sola vez la request original mediante `_retry`;
5. limpia sesión y navega a `/login` si el refresh falla.
Login, refresh, logout, session y companies available están excluidos del bucle de refresh y del header tenant. Una request tenant reintentada conserva el contexto de empresa.
## 10. Navegación y contexto
La navegación derivada de una operación pertenece al controller. Las páginas conectan callbacks y las vistas puras no importan React Router. El retorno desde editores debe reutilizar el mecanismo de `returnTo`/navegación existente en vez de reconstruir rutas localmente.
El estado remoto permanece en TanStack Query. No se duplica en estado local salvo estado efímero de presentación. Queries, invalidaciones y mutations viven en hooks compartidos del dominio.
## 11. Reglas de contribución arquitectónica
- Mantener TypeScript estricto y `import type` en cabecera.
- Naming y código en inglés; documentación y comentarios arquitectónicos en español.
- Preferir composición a frameworks CRUD o controllers genéricos.
- No introducir acceso a red en UI ni reglas de negocio en Pages.
- Mantener routing, sesión, tenant y sistema visual como responsabilidades separadas.
- Cambios de API visual pública deben sincronizar implementación, [UI system](./ui-system.md), UI Kit y tests.

View File

@ -0,0 +1,187 @@
# Patrones de UI
> **Propósito:** explicar cómo implementar pantallas concretas. Este documento, junto con [Arquitectura frontend](./frontend-architecture.md) y [UI system](./ui-system.md), forma la fuente de verdad (SSOT) de frontend/UI.
Los componentes aquí citados se definen en `ui-system.md`; este documento explica cómo componerlos.
## 1. Listados
```tsx
<RdxPageFrame>
<RdxPageContainer width="full">
<RdxPageStack>
<RdxPageHeader
title={title}
description={description}
actions={<RdxPageActions primary={primaryAction} />}
/>
{refreshError}
<DataTable topContent={toolbarAndFilters} {...tableProps} />
</RdxPageStack>
</RdxPageContainer>
</RdxPageFrame>
```
- El CTA de creación permanece durante loading, empty y errores de refresh; puede desaparecer ante error inicial bloqueante.
- Búsqueda, filtros y controles viven en `DataTable.topContent`.
- TanStack Query conserva los datos remotos y la paginación server-side.
- Búsqueda, filtros, sorting y paginación reflejan su estado relevante en `searchParams` y reinician página cuando corresponda.
- No se calculan contadores globales a partir de una página parcial.
- El click de fila recibe la fila; la página/controller usa explícitamente `row.id`.
- Una vista de composición de dominio puede encapsular el shell si permanece pura.
El controller de página concentra URL, filtros, consulta, estados y navegación. La Page define columnas y conecta callbacks; no reproduce reglas de dominio.
## 2. Create / Update
```tsx
<RdxPageFrame
header={<RdxEditorHeader title={title} onBack={onBack} />}
footer={<RdxEditorActionBar secondary={cancel} primary={submit} />}
>
<RdxPageContainer width="content">
<RdxEditorLayout aside={aside}>
<RdxPageStack>
{notice}
<RdxPanel title={sectionTitle}>{fields}</RdxPanel>
</RdxPageStack>
</RdxEditorLayout>
</RdxPageContainer>
</RdxPageFrame>
```
Footer y aside son opcionales. `content` es adecuado para formularios focalizados; `full` para editores documentales amplios. Create y Update deben compartir geometría cuando solo cambian defaults, submit o navegación.
Loading y error bloqueante usan también `RdxPageFrame` y `RdxPageContainer`; no reconstruyen el viewport.
## 3. Detail
Un detalle normal usa `RdxPageFrame`, `RdxEditorHeader` cuando es fullscreen y el container apropiado.
Un detalle documental especializado puede usar:
```text
RdxPageFrame content overflow-hidden
├── document pane → scroll interno
└── inspector → scroll interno
```
Solo se usa esta excepción cuando los panes necesitan permanecer visibles simultáneamente. No se crea un layout genérico de detalle si no existe repetición real.
## 4. Settings
Settings posee un layout funcional bajo un único frame:
```text
RdxPageFrame
└── SettingsLayout
├── header
├── navigation
└── Outlet
```
Las páginas hijas no añaden otro `RdxPageFrame` cuando el padre ya controla shell y scroll. El layout de Settings se conserva porque aporta navegación y composición funcional.
## 5. Dialogs
Usar `Dialog` para una tarea breve y focalizada que debe volver al contexto actual. Usar `AlertDialog` para confirmaciones, especialmente destructivas. Una edición prolongada o documental necesita una ruta propia.
Orden recomendado:
```text
DialogContent
├── DialogHeader: título y descripción
├── campo de identidad, si existe
├── secciones agrupadas por intención
└── DialogFooter: cancelar y enviar
```
- La cabecera y el footer se separan visualmente del formulario.
- Los bloques usan `section` y `aria-labelledby` cuando tienen título.
- Campos equivalentes pueden usar dos columnas desde `sm`; en móvil se apilan.
- El botón submit puede apuntar al formulario mediante `form` si el footer está fuera de `<form>`.
- Durante submit se deshabilitan controles y acciones de cierre que puedan interrumpir la operación.
- El controller posee defaults, validación, mutation, reset, feedback y navegación posterior.
- Todo texto visible usa i18n en `es` y `en`.
## 6. Acciones de fila
`RdxRowActions` recibe acciones ya resueltas:
- frecuentes en `primaryActions`;
- secundarias y destructivas en `overflowActions` por defecto;
- orden, `disabled`, `disabledReason`, permisos y callbacks pertenecen al consumidor;
- una acción deshabilitada puede seguir visible para explicar por qué no está disponible.
No se trasladan entidades, navegación, dialogs ni permisos a `rdx-ui`.
## 7. Loading, error y empty
| Estado | Patrón |
| --- | --- |
| Loading inicial de tabla | `DataTable loading` dentro del shell estable |
| Loading de editor | skeleton con el mismo frame/container que success |
| Error inicial bloqueante | `PageErrorState` dentro de frame y container |
| Error de refresh | `ErrorAlert` inline; mantiene datos y controles |
| Empty | mensaje/estado vacío en el stack del contenido |
No se crean sistemas alternativos de errores o skeletons cuando los componentes existentes cubren el caso.
## 8. Formularios
- React Hook Form y `FormProvider` pertenecen al consumidor.
- `RdxFormGrid` organiza únicamente campos.
- Errores de campo permanecen junto al control; errores de operación usan `ErrorAlert`.
- Dirty state, bloqueo de navegación y submit pertenecen al controller/provider correspondiente.
- La acción principal comunica loading y evita doble submit.
- Una única superficie editable es preferible a cards anidadas.
## 9. Main + aside
El aside es opcional y contiene metadata, resumen o contexto secundario. En desktop acompaña al main y en pantallas menores se apila debajo.
No usar el aside para equilibrar visualmente columnas, trasladar totales o fragmentar arbitrariamente el flujo del formulario. Tablas y line editors mantienen su layout especializado dentro del main.
## 10. Responsive
- Validar móvil, tablet y desktop.
- Evitar widths fijos en campos y controles localizados.
- Mantener acciones utilizables cuando se apilan.
- Confirmar `min-w-0`, truncado intencional y ausencia de overflow horizontal.
- En layouts especializados, comprobar pane sizes y scroll independiente.
## 11. Accesibilidad
- Controles nativos o primitives accesibles; nunca `div` clicables.
- Labels asociadas mediante `htmlFor`/`id`.
- Icon buttons con `aria-label` y tooltips cuando corresponda.
- Activación por teclado en filas y acciones.
- Foco visible, orden DOM lógico y errores anunciables.
- El color no sustituye texto, icono o estado semántico.
## 12. Testing
Usar tests de comportamiento con el runner vigente del workspace y Testing Library cuando existe interacción DOM. No probar detalles internos del controller ni clases visuales incidentales.
Cobertura mínima según patrón:
- listado: CTA, search, filtros, paginación, estados y navegación de fila;
- editor: header, contenido, acciones, dirty/submit y loading/error;
- dialog: título, campos, condicionales, cancelar/enviar y mutation;
- UI pública: contrato observable, accesibilidad y responsabilidades estructurales.
Las traducciones productivas deben probarse sin sustituirlas por un diccionario que oculte claves ausentes.
## 13. Checklist
- [ ] La ruta usa el shell real declarado en `handle.layout`.
- [ ] Existe un único `RdxPageFrame` propietario del scroll.
- [ ] Header editorial o fullscreen elegido por responsabilidad.
- [ ] Width y padding pertenecen a `RdxPageContainer`.
- [ ] El aside es funcional y opcional.
- [ ] No hay viewport o scroll principal manual en la Page.
- [ ] Loading, error, empty y success conservan estructura coherente.
- [ ] Acciones, permisos y navegación permanecen en el consumidor.
- [ ] Responsive, teclado, foco, i18n y light/dark están validados.
- [ ] Implementación, documentación, UI Kit y tests siguen sincronizados.

185
docs/frontend/ui-system.md Normal file
View File

@ -0,0 +1,185 @@
# Sistema UI Rdx
> **Propósito:** explicar qué foundations y componentes visuales existen. Este documento, junto con [Arquitectura frontend](./frontend-architecture.md) y [UI patterns](./ui-patterns.md), forma la fuente de verdad (SSOT) de frontend/UI.
## 1. Principios
- Claridad antes que abstracción.
- UI pura: recibe datos preparados y emite eventos.
- Composición antes que configuración extensa.
- KISS: no existe un framework CRUD genérico.
- Tema claro y oscuro desde el inicio.
- Accesibilidad y densidad informativa.
- Tokens semánticos en lugar de colores físicos.
## 2. Responsabilidad de `rdx-ui`
`packages/rdx-ui` contiene foundations y composiciones visuales reutilizables. No conoce React Query, routing, DTOs, entidades de dominio, controllers, permisos, casos de uso ni errores de transporte. El consumidor decide contenido, acciones, orden, labels y estados disabled.
Los componentes públicos propios usan prefijo `Rdx`, API pequeña, props explícitas y `className` solo como escape controlado.
## 3. Arquitectura visual
```text
AppMain
└── RdxPageFrame
├── header
│ ├── RdxPageHeader
│ └── RdxEditorHeader
├── content (scroll principal)
│ └── RdxPageContainer
│ ├── RdxPageStack
│ └── RdxEditorLayout
└── footer opcional
└── RdxEditorActionBar
```
Cada nivel tiene una sola responsabilidad. No se reconstruye viewport, anchura y scroll en componentes de página.
## 4. Foundations
### Espaciado y anchura
`RdxPageContainer` controla ancho, centrado y padding:
| Propiedad | Resultado |
| --- | --- |
| padding móvil | `px-4 py-4` |
| padding desktop | `md:px-6 md:py-6` |
| `width="full"` | ancho completo |
| `width="wide"` | centrado, `max-w-screen-2xl` |
| `width="content"` | centrado, `max-w-5xl` |
`padding="none"` elimina únicamente su padding. El container no controla altura ni scroll.
`RdxPageStack` establece el ritmo vertical:
| Spacing | Gap |
| --- | --- |
| `compact` | 4 |
| `default` | 6 |
| `relaxed` | 8 |
### Tema
Se usan tokens como `bg-background`, `text-foreground`, `bg-muted`, `text-muted-foreground`, `border-border`, `bg-primary` y `bg-destructive`. El color nunca es el único indicador de estado. Toda foundation nueva debe verificarse en light y dark.
## 5. Componentes públicos
### `RdxPageFrame`
Compone header, contenido y footer y es propietario del scroll principal. No conoce routing, queries, width, formularios ni negocio.
```tsx
<RdxPageFrame
header={<RdxEditorHeader title={title} />}
footer={<RdxEditorActionBar primary={saveButton} />}
>
<RdxPageContainer width="content">{content}</RdxPageContainer>
</RdxPageFrame>
```
`contentClassName="overflow-hidden"` se reserva para layouts especializados con panes y scrolls internos.
### Headers
`RdxPageHeader` es la cabecera editorial estándar. Presenta `title`, `description`, `leading`, `metadata`, `actions` y contenido opcional.
`RdxEditorHeader` es la cabecera compacta fullscreen. Presenta `title`, retorno mediante `onBack`/`backLabel`, `status`, `actions` y contenido contextual opcional.
Ninguno usa `sticky` para seleccionar una variante. El frame coloca el header fuera del scroll.
### `RdxPageActions`
Ordena regiones `secondary`, `primary` y `overflow`. No decide qué acciones existen ni sus permisos.
### `RdxEditorLayout`
Distribuye main y un `aside` opcional. No controla width, padding ni scroll. Sin aside no reserva una columna. En desktop, el aside usa `22rem`; en anchos menores se apila.
El aside aloja contexto secundario, metadata o configuración auxiliar. No se mueven campos allí solo por estética.
### `RdxEditorActionBar`
Compone `leading`, `status`, `secondary` y `primary`. El patrón normal la coloca en `RdxPageFrame.footer`, fuera del scroll. La capacidad `sticky` solo se usa si la barra vive justificadamente dentro de un área scrollable.
### `RdxPanel`
Agrupa una sección con título, descripción, icono, acciones y contenido:
- `variant="plain"`: sección plana con divisor; valor predeterminado.
- `variant="card"`: superficie independiente.
### `RdxFormGrid`
Proporciona la retícula de campos. No conoce React Hook Form ni debe envolver tablas, line editors o layouts especializados.
### `RdxRowActions`
Renderiza acciones primarias y overflow. El consumidor decide acciones, orden, navegación, `disabledReason` y destructividad. Las destructivas van en overflow por defecto.
### `RdxContextPanel`
Compone cabecera, cierre, body y footer de un panel contextual. La geometría redimensionable y el contenido de dominio permanecen en el consumidor.
## 6. Scroll y viewport
Regla canónica:
```text
AppMain (espacio disponible, sin scroll)
└── RdxPageFrame
└── content: overflow-y-auto
```
Una Page no debe usar `fixed inset-0`, `h-screen`, `h-dvh`, `overflow-y-auto` ni `overflow-auto` para reconstruir viewport o scroll principal.
Excepción: un detalle documental puede configurar el contenido del frame con `overflow-hidden` y delegar scroll interno a document panes o inspectors. Dialogs, sheets, drawers, overlays, viewers y paneles internos también pueden necesitar posicionamiento o scroll propios.
## 7. Superficies
- El contenido principal es plano por defecto.
- Las secciones se separan con spacing y divisores.
- Una card es una excepción, no el envoltorio automático de cada sección.
- El aside puede usar cards cuando necesita una superficie independiente.
- Tablas y editores de líneas conservan su layout funcional.
- Evitar card dentro de card y sombras acumuladas.
## 8. Responsive y accesibilidad
- Las columnas se apilan antes de producir overflow horizontal.
- Todo contenedor flexible susceptible de truncado usa `min-w-0`.
- Botones de icono tienen nombre accesible y foco visible.
- No se crean `div` clicables.
- El orden DOM debe ser coherente con lectura y teclado.
- Los iconos decorativos usan `aria-hidden`.
- Estados no dependen exclusivamente del color.
## 9. UI Kit
`/development/ui-kit` muestra las foundations públicas vigentes y sirve para revisar tema, responsive y composición. No es una especificación alternativa: debe mantenerse sincronizado con implementación, este documento y tests.
## 10. Testing
Los componentes públicos se prueban por comportamiento y contrato observable. No se fijan clases Tailwind salvo cuando expresan una responsabilidad estructural esencial, como el ownership de scroll o la ausencia de una columna aside.
Cada cambio de API pública debe actualizar conjuntamente:
1. implementación;
2. documentación;
3. UI Kit;
4. tests aplicables.
## 11. Anti-patterns
- Variantes genéricas `simple`/`complex` para mezclar width y geometría.
- Layouts CRUD universales.
- Scroll principal fuera de `RdxPageFrame`.
- Padding duplicado entre shell, container y superficie.
- Routing, permisos o queries dentro de `rdx-ui`.
- Acciones completas duplicadas en header y footer.
- Cards por sección sin necesidad funcional.
- Colores físicos o color como única señal.
Las recetas para construir páginas concretas están en [UI patterns](./ui-patterns.md).