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 → 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 ## 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 ## 2. Create / Update
Estructura canónica: providers → RdxPageFrame (header/content/footer) → un único RdxPageContainer → un único RdxPageContent; los dialogs son hermanos del frame.
```tsx ```tsx
<RdxPageFrame <RdxPageFrame
header={<RdxEditorHeader />} 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. 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. 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 ## 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="plain"`: sección plana con divisor; valor predeterminado.
- `variant="card"`: superficie independiente. - `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` ### `RdxFormGrid`
@ -157,7 +158,7 @@ Los avisos, errores inline, secciones y contenido funcional deben ser hijos dire
<Editor /> <Editor />
</RdxPageContent> </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 ## 8. Responsive y accesibilidad

View File

@ -18,7 +18,7 @@ Se adoptan de Customer:
- manifest y rutas lazy por módulo; - manifest y rutas lazy por módulo;
- páginas de listado con `RdxPageContainer`, `RdxPageHeader`, búsqueda con debounce y `DataTable`; - 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; - `PageErrorState`, skeletons, alertas y toasts del sistema;
- React Hook Form con validación Zod; - React Hook Form con validación Zod;
- queries y mutations encapsuladas en hooks y adapters de transporte. - 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 ## 17. Componentes reutilizados
- `RdxPageContainer`, `RdxPageContent`, `RdxPageHeader`, `RdxPageActions`; - `RdxPageContainer`, `RdxPageContent`, `RdxPageHeader`, `RdxPageActions`;
- `RdxEditorPageLayout`, `RdxPanel`, `RdxFormGrid`; - `RdxPageFrame`, `RdxPanel`, `RdxFormGrid`;
- `DataTable`, `SkeletonDataTable`, `DataTableColumnHeader`, `RdxRowActions`; - `DataTable`, `SkeletonDataTable`, `DataTableColumnHeader`, `RdxRowActions`;
- `TextField`, `SelectField`, `PageFormHeader`, `FormActionsBar`; - `TextField`, `SelectField`, `RdxEditorHeader`, `FormActionsBar`;
- `SimpleSearchInput`, `PageErrorState`, `ErrorAlert`; - `SimpleSearchInput`, `PageErrorState`, `ErrorAlert`;
- `UnsavedChangesProvider` y `useReturnToNavigation`. - `UnsavedChangesProvider` y `useReturnToNavigation`.

View File

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

View File

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

View File

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

View File

@ -40,21 +40,17 @@ export const SupplierCreatePage = () => {
> >
<RdxPageContainer width="content"> <RdxPageContainer width="content">
<RdxPageContent> <RdxPageContent>
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6"> {controller.mutation.isError ? (
<div className="flex min-w-0 flex-col gap-6"> <ErrorAlert
{controller.mutation.isError ? ( message={controller.mutation.error.message || t("create.error")}
<ErrorAlert title={t("create.error")}
message={controller.mutation.error.message || t("create.error")} />
title={t("create.error")} ) : null}
/> <SupplierEditorForm
) : null} formId={controller.formId}
<SupplierEditorForm isSubmitting={controller.mutation.isPending}
formId={controller.formId} onSubmit={controller.onSubmit}
isSubmitting={controller.mutation.isPending} />
onSubmit={controller.onSubmit}
/>
</div>
</div>
</RdxPageContent> </RdxPageContent>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </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"; import { useTranslation } from "../../i18n";
@ -19,15 +19,17 @@ export const SupplierEditorSkeleton = () => {
return ( return (
<RdxPageFrame> <RdxPageFrame>
<RdxPageContainer width="content"> <RdxPageContainer width="content">
<div aria-hidden className="space-y-6"> <RdxPageContent>
<div className="h-10 w-64 animate-pulse rounded-md bg-muted" /> <div aria-hidden className="space-y-6">
<div className="grid gap-4 md:grid-cols-2"> <div className="h-10 w-64 animate-pulse rounded-md bg-muted" />
{SKELETON_FIELDS.map((field) => ( <div className="grid gap-4 md:grid-cols-2">
<div className="h-16 animate-pulse rounded-md bg-muted" key={field} /> {SKELETON_FIELDS.map((field) => (
))} <div className="h-16 animate-pulse rounded-md bg-muted" key={field} />
))}
</div>
</div> </div>
</div> <span className="sr-only">{t("common.loading")}</span>
<span className="sr-only">{t("common.loading")}</span> </RdxPageContent>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </RdxPageFrame>
); );

View File

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