feat(ui): actualizar la estructura de RdxPageContent en varias páginas y componentes para mejorar la jerarquía y la semántica

This commit is contained in:
David Arranz 2026-09-14 19:49:07 +02:00
parent 450cc8d7bd
commit b6aa20e52b
10 changed files with 66 additions and 83 deletions

View File

@ -82,7 +82,7 @@ RdxPageContent
→ composición de los hijos principales
```
Regla: Las Pages no deben reconstruir estas responsabilidades con wrappers HTML locales.
Regla: Las Pages no deben reconstruir estas responsabilidades con wrappers HTML locales. En Create/Update, la composición canónica es RdxPageFrame → RdxPageContainer → RdxPageContent: los errores inline y el editor son hijos directos de RdxPageContent; los dialogs son hermanos del frame; RdxPanel se reserva para secciones semánticas reales.
## 6. Identity y sesión

View File

@ -34,6 +34,8 @@ El controller de página concentra URL, filtros, consulta, estados y navegación
## 2. Create / Update
Estructura canónica: providers → RdxPageFrame (header/content/footer) → un único RdxPageContainer → un único RdxPageContent; los dialogs son hermanos del frame.
```tsx
<RdxPageFrame
header={<RdxEditorHeader />}
@ -53,6 +55,7 @@ Los errores inline no definen su propio spacing estructural. Participan del spac
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.
El error bloqueante conserva la misma jerarquía: RdxPageFrame → RdxPageContainer → RdxPageContent → PageErrorState.
## 3. Detail

View File

@ -108,6 +108,7 @@ 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.
RdxPanel no sustituye un div estructural: solo representa una sección con identidad semántica, como fiscalidad, dirección o preferencias.
### `RdxFormGrid`
@ -157,7 +158,7 @@ Los avisos, errores inline, secciones y contenido funcional deben ser hijos dire
<Editor />
</RdxPageContent>
```
No deben introducirse wrappers adicionales únicamente para controlar gap, ancho, padding o centrado.
No deben introducirse wrappers adicionales únicamente para controlar gap, ancho, padding o centrado. No se anidan RdxPageContainer ni RdxPageContent salvo razón arquitectónica explícita. Los errores inline participan como hijos directos del spacing estándar de RdxPageContent; no crean su propio layout.
## 8. Responsive y accesibilidad

View File

@ -18,7 +18,7 @@ Se adoptan de Customer:
- manifest y rutas lazy por módulo;
- páginas de listado con `RdxPageContainer`, `RdxPageHeader`, búsqueda con debounce y `DataTable`;
- editores fullscreen con `RdxEditorPageLayout`, header persistente, protección de cambios sin guardar y secciones Rdx;
- editores fullscreen con `RdxPageFrame`, header persistente, protección de cambios sin guardar y secciones Rdx;
- `PageErrorState`, skeletons, alertas y toasts del sistema;
- React Hook Form con validación Zod;
- queries y mutations encapsuladas en hooks y adapters de transporte.
@ -119,9 +119,9 @@ Todos los textos visibles pertenecen al namespace `Suppliers`. Se mantienen recu
## 17. Componentes reutilizados
- `RdxPageContainer`, `RdxPageContent`, `RdxPageHeader`, `RdxPageActions`;
- `RdxEditorPageLayout`, `RdxPanel`, `RdxFormGrid`;
- `RdxPageFrame`, `RdxPanel`, `RdxFormGrid`;
- `DataTable`, `SkeletonDataTable`, `DataTableColumnHeader`, `RdxRowActions`;
- `TextField`, `SelectField`, `PageFormHeader`, `FormActionsBar`;
- `TextField`, `SelectField`, `RdxEditorHeader`, `FormActionsBar`;
- `SimpleSearchInput`, `PageErrorState`, `ErrorAlert`;
- `UnsavedChangesProvider` y `useReturnToNavigation`.

View File

@ -34,7 +34,7 @@ export const ProformaCreatePage = () => {
}
>
<RdxPageContainer width="content">
<RdxPageContent spacing="default">
<RdxPageContent>
{createCtrl.isCreateError && (
<ErrorAlert
message={

View File

@ -1,20 +1,13 @@
import { ErrorAlert } from "@erp/core/components";
import { useHookForm } from "@erp/core/hooks";
import {
RdxEditorHeader,
RdxPageContainer,
RdxPageContent,
RdxPageFrame,
RdxPanel,
} from "@repo/rdx-ui/components";
import {
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Input,
Label,
Textarea,
} from "@repo/shadcn-ui/components";
import { Button, Input, Label, Textarea } from "@repo/shadcn-ui/components";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
@ -89,6 +82,7 @@ export const RecurringProformaCreatePage = () => {
>
<RdxPageContainer width="content">
<RdxPageContent>
{submitError ? <ErrorAlert message={submitError} /> : null}
<form
className="space-y-6"
id={RECURRING_PROFORMA_CREATE_FORM_ID}
@ -98,19 +92,8 @@ export const RecurringProformaCreatePage = () => {
<p className="text-sm text-muted-foreground">
{t("recurring_proformas.create.description")}
</p>
{submitError ? (
<p
className="rounded-md border border-destructive/30 p-3 text-sm text-destructive"
role="alert"
>
{submitError}
</p>
) : null}
<Card>
<CardHeader>
<CardTitle>{t("recurring_proformas.create.form_title")}</CardTitle>
</CardHeader>
<CardContent className="space-y-5">
<RdxPanel title={t("recurring_proformas.create.form_title")}>
<div className="space-y-5">
<div className="space-y-2">
<Label htmlFor="recurring-proforma-name">
{t("recurring_proformas.create.name")}
@ -136,8 +119,8 @@ export const RecurringProformaCreatePage = () => {
{...form.register("description")}
/>
</div>
</CardContent>
</Card>
</div>
</RdxPanel>
</form>
</RdxPageContent>
</RdxPageContainer>

View File

@ -47,7 +47,7 @@ export const CustomerCreatePage = () => {
}
>
<RdxPageContainer width="content">
<RdxPageContent spacing="default">
<RdxPageContent>
{createCtrl.isCreateError ? (
<ErrorAlert
message={(createCtrl.createError as Error)?.message ?? t("create.errors.message")}

View File

@ -40,21 +40,17 @@ export const SupplierCreatePage = () => {
>
<RdxPageContainer width="content">
<RdxPageContent>
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
<div className="flex min-w-0 flex-col gap-6">
{controller.mutation.isError ? (
<ErrorAlert
message={controller.mutation.error.message || t("create.error")}
title={t("create.error")}
/>
) : null}
<SupplierEditorForm
formId={controller.formId}
isSubmitting={controller.mutation.isPending}
onSubmit={controller.onSubmit}
/>
</div>
</div>
{controller.mutation.isError ? (
<ErrorAlert
message={controller.mutation.error.message || t("create.error")}
title={t("create.error")}
/>
) : null}
<SupplierEditorForm
formId={controller.formId}
isSubmitting={controller.mutation.isPending}
onSubmit={controller.onSubmit}
/>
</RdxPageContent>
</RdxPageContainer>
</RdxPageFrame>

View File

@ -1,4 +1,4 @@
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { RdxPageContainer, RdxPageContent, RdxPageFrame } from "@repo/rdx-ui/components";
import { useTranslation } from "../../i18n";
@ -19,15 +19,17 @@ export const SupplierEditorSkeleton = () => {
return (
<RdxPageFrame>
<RdxPageContainer width="content">
<div aria-hidden className="space-y-6">
<div className="h-10 w-64 animate-pulse rounded-md bg-muted" />
<div className="grid gap-4 md:grid-cols-2">
{SKELETON_FIELDS.map((field) => (
<div className="h-16 animate-pulse rounded-md bg-muted" key={field} />
))}
<RdxPageContent>
<div aria-hidden className="space-y-6">
<div className="h-10 w-64 animate-pulse rounded-md bg-muted" />
<div className="grid gap-4 md:grid-cols-2">
{SKELETON_FIELDS.map((field) => (
<div className="h-16 animate-pulse rounded-md bg-muted" key={field} />
))}
</div>
</div>
</div>
<span className="sr-only">{t("common.loading")}</span>
<span className="sr-only">{t("common.loading")}</span>
</RdxPageContent>
</RdxPageContainer>
</RdxPageFrame>
);

View File

@ -32,18 +32,20 @@ export const SupplierUpdatePage = () => {
return (
<RdxPageFrame>
<RdxPageContainer width="content">
<PageErrorState
description={t("detail.error.description")}
primaryAction={{
label: t("common.actions.retry"),
onClick: async () => {
await controller.query.refetch();
},
}}
secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }}
title={t("detail.error.title")}
variant="notFound"
/>
<RdxPageContent>
<PageErrorState
description={t("detail.error.description")}
primaryAction={{
label: t("common.actions.retry"),
onClick: async () => {
await controller.query.refetch();
},
}}
secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }}
title={t("detail.error.title")}
variant="notFound"
/>
</RdxPageContent>
</RdxPageContainer>
</RdxPageFrame>
);
@ -66,21 +68,17 @@ export const SupplierUpdatePage = () => {
>
<RdxPageContainer width="content">
<RdxPageContent>
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
<div className="flex min-w-0 flex-col gap-6">
{controller.mutation.isError ? (
<ErrorAlert
message={controller.mutation.error.message || t("update.error")}
title={t("update.error")}
/>
) : null}
<SupplierEditorForm
formId={controller.formId}
isSubmitting={controller.mutation.isPending}
onSubmit={controller.onSubmit}
/>
</div>
</div>
{controller.mutation.isError ? (
<ErrorAlert
message={controller.mutation.error.message || t("update.error")}
title={t("update.error")}
/>
) : null}
<SupplierEditorForm
formId={controller.formId}
isSubmitting={controller.mutation.isPending}
onSubmit={controller.onSubmit}
/>
</RdxPageContent>
</RdxPageContainer>
</RdxPageFrame>