refactor: reemplazar RdxEditorPageLayout con RdxPageContainer y RdxEditorLayout en múltiples componentes

This commit is contained in:
David Arranz 2026-09-04 13:48:16 +02:00
parent 2f8122ca77
commit 3aa3f4018b
14 changed files with 343 additions and 295 deletions

View File

@ -2,7 +2,7 @@ import {
RdxBadge,
RdxContextPanel,
RdxEditorActionBar,
RdxEditorPageLayout,
RdxEditorLayout,
RdxFormGrid,
RdxPageActions,
RdxPageContainer,
@ -751,13 +751,15 @@ export default function UiKitPage() {
title="Editor foundations"
>
<div className="grid gap-4">
<ExampleCard title="RdxEditorPageLayout simple">
<ExampleCard title="RdxPageContainer + RdxEditorLayout">
<div className="rounded-lg border border-border bg-muted/30">
<RdxEditorPageLayout complexity="simple">
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
Centered editor content
</div>
</RdxEditorPageLayout>
<RdxPageContainer width="content">
<RdxEditorLayout>
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
Centered editor content
</div>
</RdxEditorLayout>
</RdxPageContainer>
</div>
</ExampleCard>
@ -806,47 +808,49 @@ export default function UiKitPage() {
title="Simple editor composition"
>
<div className="rounded-lg border border-border bg-muted/30">
<RdxEditorPageLayout complexity="simple">
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<div className="flex min-w-0 flex-col gap-6">
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
Save error aligned with editor width
</div>
<RdxPageContainer width="content">
<RdxEditorLayout>
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<div className="flex min-w-0 flex-col gap-6">
<RdxPanel
description="Primary customer information"
icon={<PlusIcon className="size-4" />}
title="Basic information"
>
<RdxFormGrid className="md:grid-cols-2">
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Name field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Tax ID field
</div>
</RdxFormGrid>
</RdxPanel>
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
Save error aligned with editor width
</div>
<RdxPanel
description="Secondary preferences"
icon={<PlusIcon className="size-4" />}
title="Preferences"
>
<RdxFormGrid className="md:grid-cols-2">
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Language field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Currency field
</div>
</RdxFormGrid>
</RdxPanel>
<div className="flex min-w-0 flex-col gap-6">
<RdxPanel
description="Primary customer information"
icon={<PlusIcon className="size-4" />}
title="Basic information"
>
<RdxFormGrid className="md:grid-cols-2">
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Name field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Tax ID field
</div>
</RdxFormGrid>
</RdxPanel>
<RdxPanel
description="Secondary preferences"
icon={<PlusIcon className="size-4" />}
title="Preferences"
>
<RdxFormGrid className="md:grid-cols-2">
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Language field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
Currency field
</div>
</RdxFormGrid>
</RdxPanel>
</div>
</div>
</div>
</div>
</RdxEditorPageLayout>
</RdxEditorLayout>
</RdxPageContainer>
</div>
</ExampleCard>
@ -870,61 +874,65 @@ export default function UiKitPage() {
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
<div className="min-h-0 flex-1 overflow-y-auto">
<RdxEditorPageLayout
aside={
<div className="flex min-w-0 flex-col gap-6">
<RdxPanel description="Secondary recipient controls." title="Recipient">
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
Customer selector and helper actions
</div>
</RdxPanel>
<RdxPageContainer width="full">
<RdxEditorLayout
aside={
<div className="flex min-w-0 flex-col gap-6">
<RdxPanel
description="Secondary recipient controls."
title="Recipient"
>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
Customer selector and helper actions
</div>
</RdxPanel>
<RdxPanel
description="Fiscal defaults and toggles."
title="Tax settings"
>
<RdxFormGrid>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
Tax regime field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
Default tax selector
</div>
</RdxFormGrid>
</RdxPanel>
</div>
}
complexity="complex"
>
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<div className="flex min-w-0 flex-col gap-6">
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
Inline feedback aligned with the editor content
<RdxPanel
description="Fiscal defaults and toggles."
title="Tax settings"
>
<RdxFormGrid>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
Tax regime field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
Default tax selector
</div>
</RdxFormGrid>
</RdxPanel>
</div>
<RdxPanel title="Document information">
<RdxFormGrid>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
Reference field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
Date field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
Series field
</div>
</RdxFormGrid>
</RdxPanel>
<section className="min-w-0 border-b border-border pb-6">
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
Line editor keeps its own internal table layout and stays in the
main surface.
}
>
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<div className="flex min-w-0 flex-col gap-6">
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
Inline feedback aligned with the editor content
</div>
</section>
<RdxPanel title="Document information">
<RdxFormGrid>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
Reference field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
Date field
</div>
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
Series field
</div>
</RdxFormGrid>
</RdxPanel>
<section className="min-w-0 border-b border-border pb-6">
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
Line editor keeps its own internal table layout and stays in the
main surface.
</div>
</section>
</div>
</div>
</div>
</RdxEditorPageLayout>
</RdxEditorLayout>
</RdxPageContainer>
</div>
<RdxEditorActionBar

View File

@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import test from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { ProformaCreateSkeleton } from "./proforma-create-skeleton";
test("Proforma Create skeleton comparte el frame del editor productivo", () => {
const html = renderToStaticMarkup(<ProformaCreateSkeleton />);
assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
assert.match(html, /rdx-page-container/);
assert.doesNotMatch(html, /fixed inset-0/);
assert.equal(html.match(/overflow-y-auto/g)?.length, 1);
});

View File

@ -1,16 +1,25 @@
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { Skeleton } from "@repo/shadcn-ui/components";
export const ProformaCreateSkeleton = () => {
return (
<div className="fixed inset-0 flex flex-col overflow-hidden">
<div className="border-b px-6 py-4">
<Skeleton className="h-10 w-full max-w-md" />
</div>
<div className="flex-1 space-y-6 overflow-y-auto p-6">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-48 w-full" />
</div>
</div>
<RdxPageFrame
contentClassName="bg-muted/20"
header={
<div className="border-b px-6 py-4">
<Skeleton className="h-10 w-full max-w-md" />
</div>
}
>
<RdxPageContainer width="content">
<RdxEditorLayout>
<div className="space-y-6">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-48 w-full" />
</div>
</RdxEditorLayout>
</RdxPageContainer>
</RdxPageFrame>
);
};

View File

@ -18,10 +18,10 @@ import { useCreateRecurringProformaFromProforma } from "../../../recurring-profo
import { buildRecurringProformaDetailPath } from "../../../recurring-proformas/shared/recurring-proforma-navigation";
import {
buildCreateRecurringProformaFromProformaDefaultValues,
buildCreateRecurringProformaFromProformaInput,
type CreateRecurringProformaFromProformaForm,
CreateRecurringProformaFromProformaFormSchema,
buildCreateRecurringProformaFromProformaDefaultValues,
buildCreateRecurringProformaFromProformaInput,
} from "./use-create-recurring-proforma-from-proforma-dialog-controller.utils";
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
@ -41,6 +41,9 @@ export interface CreateRecurringProformaFromProformaDialogControllerParams {
proformaReference: string | null;
}
type CreateRecurringProformaFromProformaTarget =
CreateRecurringProformaFromProformaDialogControllerParams;
export const useCreateRecurringProformaFromProformaDialogController = ({
proformaId,
proformaReference,
@ -51,6 +54,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
const mutation = useCreateRecurringProformaFromProforma();
const [open, setOpen] = useState(false);
const submitInProgressRef = useRef(false);
const targetRef = useRef<CreateRecurringProformaFromProformaTarget>({
proformaId,
proformaReference,
});
const form = useHookForm<CreateRecurringProformaFromProformaForm>({
resolverSchema: CreateRecurringProformaFromProformaFormSchema,
initialValues: buildCreateRecurringProformaFromProformaDefaultValues(proformaReference),
@ -77,8 +84,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
);
};
const openDialog = () => {
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(proformaReference));
const openDialog = (target?: CreateRecurringProformaFromProformaTarget) => {
const nextTarget = target ?? { proformaId, proformaReference };
targetRef.current = nextTarget;
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(nextTarget.proformaReference));
setOpen(true);
};
@ -100,7 +109,7 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
try {
const created = await mutation.mutateAsync({
proformaId,
proformaId: targetRef.current.proformaId,
data: buildCreateRecurringProformaFromProformaInput(values),
});

View File

@ -1,13 +1,14 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isValidElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import type { CellContext } from "@tanstack/react-table";
import { RdxRowActions, type RdxRowActionsProps } from "@repo/rdx-ui/components";
import type { CellContext } from "@tanstack/react-table";
import { isValidElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import type { ProformaListRow } from "../../../../shared";
import { PROFORMA_STATUS } from "../../../../shared";
import { useProformasGridColumns } from "./use-proforma-grid-columns";
const proformaFixture: ProformaListRow = {
@ -104,12 +105,16 @@ test("mantiene las destructivas en overflow y conserva disabled", () => {
assert.equal(viewAction?.disabled, true);
});
test("preserva callbacks de view, edit y delete", () => {
test("preserva callbacks de view, edit, crear recurrencia y delete", () => {
let didView = false;
let didEdit = false;
let didCreateRecurring = false;
let didDelete = false;
const props = getRowActionsProps("normal", {
onCreateRecurringClick: () => {
didCreateRecurring = true;
},
onDeleteClick: () => {
didDelete = true;
},
@ -123,9 +128,29 @@ test("preserva callbacks de view, edit y delete", () => {
props.primaryActions?.find((action) => action.id === "view")?.onSelect();
props.primaryActions?.find((action) => action.id === "edit")?.onSelect();
props.overflowActions?.find((action) => action.id === "create-recurring")?.onSelect();
props.overflowActions?.find((action) => action.id === "delete")?.onSelect();
assert.equal(didView, true);
assert.equal(didEdit, true);
assert.equal(didCreateRecurring, true);
assert.equal(didDelete, true);
});
test("solo permite crear recurrencia desde el listado normal", () => {
const normalProps = getRowActionsProps("normal", {
onCreateRecurringClick: () => undefined,
});
const archivedProps = getRowActionsProps("archived", {
onCreateRecurringClick: () => undefined,
});
assert.equal(
normalProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
false
);
assert.equal(
archivedProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
true
);
});

View File

@ -1,12 +1,10 @@
import { EmptyCellValue } from "@erp/core/components";
import { DataTableColumnHeader, RdxRowActions, type RdxRowAction } from "@repo/rdx-ui/components";
import { DataTableColumnHeader, type RdxRowAction, RdxRowActions } from "@repo/rdx-ui/components";
import { DateHelper } from "@repo/rdx-utils";
import {
Checkbox,
Spinner,
} from "@repo/shadcn-ui/components";
import { Checkbox, Spinner } from "@repo/shadcn-ui/components";
import type { ColumnDef } from "@tanstack/react-table";
import {
CalendarSyncIcon,
CopyIcon,
ExternalLinkIcon,
EyeIcon,
@ -37,6 +35,7 @@ type GridActionHandlers = {
onIssueClick?: (proforma: ProformaListRow) => void;
onArchiveClick?: (proforma: ProformaListRow) => void;
onDuplicateClick?: (proforma: ProformaListRow) => void;
onCreateRecurringClick?: (proforma: ProformaListRow) => void;
onUnarchiveClick?: (proforma: ProformaListRow) => void;
onDeleteClick?: (proforma: ProformaListRow) => void;
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
@ -52,10 +51,8 @@ type GridActionHandlers = {
isPdfDownloading?: boolean;
};
const renderActionIcon = (
Icon: ComponentType<{ className?: string }>,
loading?: boolean
) => (loading ? <Spinner className="size-4" /> : <Icon className="size-4" />);
const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
loading ? <Spinner className="size-4" /> : <Icon className="size-4" />;
export function useProformasGridColumns(
scope: ProformaListScope,
@ -312,6 +309,7 @@ export function useProformasGridColumns(
const handleView = () => actionHandlers.onViewClick?.(proforma);
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
@ -416,6 +414,14 @@ export function useProformasGridColumns(
: undefined,
onSelect: handleDuplicate,
},
{
id: "create-recurring",
label: t("pages.proformas.list.actions.create_recurring"),
icon: renderActionIcon(CalendarSyncIcon),
disabled: !(actionHandlers.onCreateRecurringClick && isNormalScope),
disabledReason: isNormalScope ? undefined : getScopeReason(),
onSelect: handleCreateRecurring,
},
{
id: "archive",
label: t("pages.proformas.list.actions.archive"),

View File

@ -32,6 +32,8 @@ import { ChangeProformaStatusDialog } from "../../../change-status";
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
import { DeleteProformaDialog } from "../../../delete";
import { prepareDeleteProformaTargets } from "../../../delete/utils";
import { useCreateRecurringProformaFromProformaDialogController } from "../../../detail/controllers";
import { CreateRecurringProformaFromProformaDialog } from "../../../detail/ui/components";
import { downloadProformaPDFApi } from "../../../download-pdf/api";
import { IssueProformaDialog } from "../../../issue-proforma";
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
@ -238,6 +240,10 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
goHome,
isRetrying,
} = useListProformasPageController(scope);
const createRecurringDialogCtrl = useCreateRecurringProformaFromProformaDialogController({
proformaId: "",
proformaReference: null,
});
const handleEditClick = (proformaId: string) => {
navigate({
@ -293,6 +299,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
onEditClick: (proforma) => handleEditClick(proforma.id),
onDuplicateClick: handleDuplicateClick,
onCreateRecurringClick: (proforma) =>
createRecurringDialogCtrl.openDialog({
proformaId: proforma.id,
proformaReference: proforma.proformaReference,
}),
onIssueClick: (proformaRow) =>
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
onArchiveClick: archiveDialogCtrl.openDialog,
@ -550,6 +561,21 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
targets={changeStatusDialogCtrl.targets}
/>
<CreateRecurringProformaFromProformaDialog
form={createRecurringDialogCtrl.form}
formId={createRecurringDialogCtrl.formId}
isSubmitting={createRecurringDialogCtrl.isSubmitting}
onOpenChange={(open) => {
if (!open) {
createRecurringDialogCtrl.closeDialog();
}
}}
onSubmit={createRecurringDialogCtrl.onSubmit}
onUnitChange={createRecurringDialogCtrl.changeUnit}
open={createRecurringDialogCtrl.open}
unit={createRecurringDialogCtrl.unit}
/>
<DeleteProformaDialog
isSubmitting={deleteDialogCtrl.isSubmitting}
onConfirm={deleteDialogCtrl.confirmDelete}

View File

@ -88,7 +88,7 @@ Contenido visible actual:
- información personal: `name`, `email`, `language_code`
- modo `app-fullscreen`, sin sidebar principal
- contenido centrado con ancho controlado mediante `RdxEditorPageLayout`
- contenido centrado mediante `RdxPageContainer` y compuesto con `RdxEditorLayout`
- seguridad mediante dialog para cambio de contraseña
- cierre de sesión reutilizando el flujo de logout existente

View File

@ -0,0 +1,65 @@
import { PageErrorState } from "@erp/core/components";
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import type { ComponentProps } from "react";
export const AccountLoadingState = ({ label }: { label: string }) => (
<RdxPageFrame contentClassName="bg-muted/20">
<RdxPageContainer className="min-h-full" width="content">
<RdxEditorLayout>
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
<div aria-busy="true" role="status">
{label}
</div>
</div>
</RdxEditorLayout>
</RdxPageContainer>
</RdxPageFrame>
);
interface AccountErrorStateProps {
canRetry: boolean;
description: string;
isRetrying: boolean;
onRetry: () => void;
reference?: string;
retryLabel: string;
retryingLabel: string;
title: string;
variant: ComponentProps<typeof PageErrorState>["variant"];
}
export const AccountErrorState = ({
canRetry,
description,
isRetrying,
onRetry,
reference,
retryLabel,
retryingLabel,
title,
variant,
}: AccountErrorStateProps) => (
<RdxPageFrame contentClassName="bg-muted/20">
<RdxPageContainer width="content">
<RdxEditorLayout>
<div className="mx-auto w-full max-w-3xl">
<PageErrorState
description={description}
primaryAction={
canRetry
? {
label: isRetrying ? retryingLabel : retryLabel,
onClick: onRetry,
isLoading: isRetrying,
}
: undefined
}
reference={reference}
title={title}
variant={variant}
/>
</div>
</RdxEditorLayout>
</RdxPageContainer>
</RdxPageFrame>
);

View File

@ -0,0 +1,36 @@
import assert from "node:assert/strict";
import test from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { AccountErrorState, AccountLoadingState } from "./account-page-states";
test("Account loading usa el frame y contenedor definitivos", () => {
const html = renderToStaticMarkup(<AccountLoadingState label="Cargando cuenta" />);
assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
assert.match(html, /rdx-page-container/);
assert.match(html, /Cargando cuenta/);
assert.doesNotMatch(html, /fixed inset-0/);
assert.doesNotMatch(html, /rdx-editor-page-layout/);
});
test("Account error permanece dentro del frame actual", () => {
const html = renderToStaticMarkup(
<AccountErrorState
canRetry
description="No se pudo cargar la cuenta"
isRetrying={false}
onRetry={() => undefined}
retryingLabel="Reintentando"
retryLabel="Reintentar"
title="Error de cuenta"
variant="server"
/>
);
assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
assert.match(html, /Error de cuenta/);
assert.match(html, /Reintentar/);
assert.doesNotMatch(html, /fixed inset-0/);
});

View File

@ -1,10 +1,12 @@
import { ErrorAlert, PageErrorState } from "@erp/core/components";
import { ErrorAlert } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import {
CancelActionButton,
RdxEditorActionBar,
RdxEditorHeader,
RdxEditorPageLayout,
RdxEditorLayout,
RdxPageContainer,
RdxPageFrame,
RdxPanel,
RhfSubmitActionButton,
SelectField,
@ -27,65 +29,7 @@ import { useTranslation } from "../../../i18n";
import { useAccountPageController } from "../../controllers";
import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
const AccountLoadingState = ({ label }: { label: string }) => {
return (
<div className="flex-1 overflow-y-auto bg-muted/20">
<RdxEditorPageLayout className="min-h-full" width="simple">
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
<div aria-busy="true" role="status">
{label}
</div>
</div>
</RdxEditorPageLayout>
</div>
);
};
const AccountErrorState = ({
canRetry,
description,
isRetrying,
onRetry,
reference,
retryLabel,
retryingLabel,
title,
variant,
}: {
canRetry: boolean;
description: string;
isRetrying: boolean;
onRetry: () => void;
reference?: string;
retryLabel: string;
retryingLabel: string;
title: string;
variant: React.ComponentProps<typeof PageErrorState>["variant"];
}) => {
return (
<div className="flex-1 overflow-y-auto bg-muted/20">
<RdxEditorPageLayout width="simple">
<div className="mx-auto w-full max-w-3xl">
<PageErrorState
description={description}
primaryAction={
canRetry
? {
label: isRetrying ? retryingLabel : retryLabel,
onClick: onRetry,
isLoading: isRetrying,
}
: undefined
}
reference={reference}
title={title}
variant={variant}
/>
</div>
</RdxEditorPageLayout>
</div>
);
};
import { AccountErrorState, AccountLoadingState } from "./account-page-states";
const AccountPasswordDialog = ({
ctrl,
@ -252,15 +196,18 @@ export const AccountPage = () => {
};
return (
<div className="fixed inset-0 flex flex-col overflow-hidden">
<RdxEditorHeader
backLabel={t("account.header.back")}
onBack={navigateBack}
title={t("account.title")}
/>
<div className="flex-1 overflow-y-auto bg-muted/20">
<RdxEditorPageLayout width="simple">
<RdxPageFrame
contentClassName="bg-muted/20"
header={
<RdxEditorHeader
backLabel={t("account.header.back")}
onBack={navigateBack}
title={t("account.title")}
/>
}
>
<RdxPageContainer width="content">
<RdxEditorLayout>
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<div className="flex min-w-0 flex-col gap-6">
<FormProvider {...ctrl.profileForm}>
@ -405,8 +352,8 @@ export const AccountPage = () => {
</RdxPanel>
</div>
</div>
</RdxEditorPageLayout>
</div>
</RdxEditorLayout>
</RdxPageContainer>
<AccountPasswordDialog
ctrl={ctrl}
@ -415,6 +362,6 @@ export const AccountPage = () => {
onOpenChange={handlePasswordDialogOpenChange}
t={t}
/>
</div>
</RdxPageFrame>
);
};

View File

@ -1,5 +1,4 @@
export * from "./rdx-editor-action-bar.tsx";
export * from "./rdx-editor-header.tsx";
export * from "./rdx-editor-layout.tsx";
export * from "./rdx-editor-page-layout.tsx";
export * from "./rdx-panel.tsx";

View File

@ -8,7 +8,6 @@ import { RdxFormGrid } from "../form/rdx-form-grid.tsx";
import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
import { RdxEditorHeader } from "./rdx-editor-header.tsx";
import { RdxEditorLayout } from "./rdx-editor-layout.tsx";
import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx";
test("RdxEditorLayout only distributes main content and an optional aside", () => {
const html = renderToStaticMarkup(
@ -50,44 +49,6 @@ test("RdxEditorHeader no renderiza back sin callback", () => {
import { RdxPanel } from "./rdx-panel.tsx";
test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza children", () => {
const html = renderToStaticMarkup(
<RdxEditorPageLayout>
<div>Editor content</div>
</RdxEditorPageLayout>
);
assert.match(html, /Editor content/);
assert.match(html, /max-w-5xl/);
assert.match(html, /px-4/);
assert.match(html, /md:px-6/);
});
test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
const html = renderToStaticMarkup(
<RdxEditorPageLayout aside={<div>Aside content</div>} width="full">
<div>Main content</div>
</RdxEditorPageLayout>
);
assert.match(html, /Main content/);
assert.match(html, /Aside content/);
assert.match(html, /xl:grid-cols-\[minmax\(0,1fr\)_22rem\]/);
assert.doesNotMatch(html, /max-w-5xl/);
});
test("RdxEditorPageLayout no reserva aside cuando no existe incluso en complex", () => {
const html = renderToStaticMarkup(
<RdxEditorPageLayout width="full">
<div>Main content</div>
</RdxEditorPageLayout>
);
assert.match(html, /Main content/);
assert.doesNotMatch(html, /<aside/);
assert.doesNotMatch(html, /grid-cols-\[/);
});
test("RdxPanel plain renderiza header semántico y contenido sin card", () => {
const html = renderToStaticMarkup(
<RdxPanel

View File

@ -1,58 +0,0 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
export type RdxEditorWidth = "wide" | "full";
export interface RdxEditorPageLayoutProps
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
children: ReactNode;
aside?: ReactNode;
width?: RdxEditorWidth;
}
const CONTENT_WIDTH_CLASSNAME: Record<RdxEditorWidth, string> = {
wide: "mx-auto w-full max-w-5xl",
full: "w-full",
};
const ASIDE_GRID_CLASSNAME: Record<RdxEditorWidth, string> = {
wide: "xl:grid-cols-[minmax(0,1fr)_280px]",
full: "xl:grid-cols-[minmax(0,1fr)_22rem]",
};
/**
* @deprecated
* Distribuye el contenido interno de un contenedor en:
*
* - una región principal
* - un aside opcional.
*
* No controla padding, scroll ni estructura general de página.
* Para nuevos consumidores usar RdxPageContainer + RdxEditorLayout.
*/
export const RdxEditorPageLayout = ({
children,
aside,
width = "wide",
className,
...props
}: RdxEditorPageLayoutProps) => {
const contentClassName = CONTENT_WIDTH_CLASSNAME[width];
return (
<div
className={cn("rdx-editor-page-layout min-w-0 w-full px-4 py-4 md:px-6 md:py-6", className)}
{...props}
>
{aside ? (
<div className={cn(contentClassName, "grid min-w-0 gap-6", ASIDE_GRID_CLASSNAME[width])}>
<div className="min-w-0">{children}</div>
<aside className="min-w-0">{aside}</aside>
</div>
) : (
<div className={cn("min-w-0", contentClassName)}>{children}</div>
)}
</div>
);
};