Compare commits

...

4 Commits

Author SHA1 Message Date
3aa3f4018b refactor: reemplazar RdxEditorPageLayout con RdxPageContainer y RdxEditorLayout en múltiples componentes 2026-09-04 13:48:16 +02:00
2f8122ca77 refactor: replace PageFormHeader with RdxEditorHeader in customer and supplier pages
- Updated CustomerViewHeader to use RdxEditorHeader for a more consistent header layout.
- Refactored CustomerViewPage to utilize RdxPageFrame and RdxPageContainer for improved structure.
- Replaced PageFormHeader with RdxEditorHeader in AccountPage for uniformity.
- Adjusted SupplierCreatePage and SupplierUpdatePage to use RdxPageFrame and RdxPageContainer for layout consistency.
- Removed SupplierLayout component as it was no longer necessary.
- Enhanced SupplierDetailPage to utilize RdxPageFrame for better layout management.
- Introduced RdxEditorHeader component to encapsulate header functionality for editor pages.
- Updated tests to cover new RdxEditorHeader functionality and removed obsolete tests related to RdxPageHeader.
2026-09-04 13:30:22 +02:00
85d2f11bde refactor: reorganizar importaciones y eliminar componentes obsoletos en las páginas de proformas y facturas 2026-09-04 11:53:59 +02:00
3eb7c37a7b Refactor proforma-related components and layouts
- Simplified the AppFullscreenLayout documentation.
- Removed ProformaLayout and refactored routes to use Outlet directly.
- Updated ProformaCreateCustomerField styles for better visibility.
- Adjusted ProformaCreateHeader to hide keyboard shortcuts button on small screens.
- Replaced FormSectionCard and FormSectionGrid with RdxPanel and RdxFormGrid in ProformaCreateInitialInfoCard.
- Refactored ProformaCreatePage to use RdxPageFrame for better layout control.
- Enhanced ProformaDetailContent and ProformaDetailPage for improved responsiveness.
- Updated ScopedListProformasPageView to use RdxPageFrame for consistent layout.
- Added new RdxEditorLayout and RdxPageFrame components for better structure in editor pages.
- Adjusted tests to accommodate new layout components and ensure proper rendering.
2026-09-04 11:41:37 +02:00
81 changed files with 1702 additions and 1651 deletions

View File

@ -2,7 +2,7 @@ import {
RdxBadge, RdxBadge,
RdxContextPanel, RdxContextPanel,
RdxEditorActionBar, RdxEditorActionBar,
RdxEditorPageLayout, RdxEditorLayout,
RdxFormGrid, RdxFormGrid,
RdxPageActions, RdxPageActions,
RdxPageContainer, RdxPageContainer,
@ -751,13 +751,15 @@ export default function UiKitPage() {
title="Editor foundations" title="Editor foundations"
> >
<div className="grid gap-4"> <div className="grid gap-4">
<ExampleCard title="RdxEditorPageLayout simple"> <ExampleCard title="RdxPageContainer + RdxEditorLayout">
<div className="rounded-lg border border-border bg-muted/30"> <div className="rounded-lg border border-border bg-muted/30">
<RdxEditorPageLayout complexity="simple"> <RdxPageContainer width="content">
<RdxEditorLayout>
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground"> <div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
Centered editor content Centered editor content
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</RdxPageContainer>
</div> </div>
</ExampleCard> </ExampleCard>
@ -806,7 +808,8 @@ export default function UiKitPage() {
title="Simple editor composition" title="Simple editor composition"
> >
<div className="rounded-lg border border-border bg-muted/30"> <div className="rounded-lg border border-border bg-muted/30">
<RdxEditorPageLayout complexity="simple"> <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="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="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"> <div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
@ -846,7 +849,8 @@ export default function UiKitPage() {
</div> </div>
</div> </div>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</RdxPageContainer>
</div> </div>
</ExampleCard> </ExampleCard>
@ -870,10 +874,14 @@ export default function UiKitPage() {
<div className="flex min-h-0 flex-1 flex-col bg-muted/20"> <div className="flex min-h-0 flex-1 flex-col bg-muted/20">
<div className="min-h-0 flex-1 overflow-y-auto"> <div className="min-h-0 flex-1 overflow-y-auto">
<RdxEditorPageLayout <RdxPageContainer width="full">
<RdxEditorLayout
aside={ aside={
<div className="flex min-w-0 flex-col gap-6"> <div className="flex min-w-0 flex-col gap-6">
<RdxPanel description="Secondary recipient controls." title="Recipient"> <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"> <div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
Customer selector and helper actions Customer selector and helper actions
</div> </div>
@ -894,7 +902,6 @@ export default function UiKitPage() {
</RdxPanel> </RdxPanel>
</div> </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="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="flex min-w-0 flex-col gap-6">
@ -924,7 +931,8 @@ export default function UiKitPage() {
</section> </section>
</div> </div>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</RdxPageContainer>
</div> </div>
<RdxEditorActionBar <RdxEditorActionBar

View File

@ -1,5 +1,10 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { RdxPageContainer, RdxPageHeader, RdxPageStack } from "@repo/rdx-ui/components"; import {
RdxPageContainer,
RdxPageFrame,
RdxPageHeader,
RdxPageStack,
} from "@repo/rdx-ui/components";
import { Skeleton } from "@repo/shadcn-ui/components"; import { Skeleton } from "@repo/shadcn-ui/components";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
@ -28,6 +33,7 @@ const HomeKpisSkeleton = () => {
); );
}; };
/** Renderiza el dashboard dentro del frame estándar de página. */
export const HomePage = () => { export const HomePage = () => {
const { t } = useAppTranslation(); const { t } = useAppTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -35,8 +41,8 @@ export const HomePage = () => {
if (ctrl.hasBlockingError && ctrl.pageError) { if (ctrl.hasBlockingError && ctrl.pageError) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={ctrl.pageError.description} description={ctrl.pageError.description}
primaryAction={{ primaryAction={{
@ -50,12 +56,13 @@ export const HomePage = () => {
title={ctrl.pageError.title} title={ctrl.pageError.title}
variant="server" variant="server"
/> />
</div>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
} }
return ( return (
<RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
<RdxPageHeader <RdxPageHeader
@ -108,5 +115,6 @@ export const HomePage = () => {
</p> </p>
</RdxPageStack> </RdxPageStack>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -12,8 +12,7 @@ export const AppFullscreenLayoutFrame = ({ children }: AppFullscreenLayoutFrameP
}; };
/** /**
* Shell de aplicación para páginas que ocupan toda el área disponible. * Shell para páginas fullscreen sin sidebar ni topbar global.
* Se utiliza en editores y vistas de detalle sin sidebar ni topbar global.
*/ */
export const AppFullscreenLayout = () => { export const AppFullscreenLayout = () => {

View File

@ -1,4 +1,9 @@
import { RdxPageContainer, RdxPageHeader, RdxPageStack } from "@repo/rdx-ui/components"; import {
RdxPageContainer,
RdxPageFrame,
RdxPageHeader,
RdxPageStack,
} from "@repo/rdx-ui/components";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import type { SettingsNavigationSection } from "@/settings/build-settings-navigation"; import type { SettingsNavigationSection } from "@/settings/build-settings-navigation";
@ -11,10 +16,14 @@ interface SettingsLayoutProps {
sections: SettingsNavigationSection[]; sections: SettingsNavigationSection[];
} }
/**
* Compone la navegación y el contenido de configuración dentro del frame estándar de página.
*/
export const SettingsLayout = ({ sections }: SettingsLayoutProps) => { export const SettingsLayout = ({ sections }: SettingsLayoutProps) => {
const { t } = useAppTranslation(); const { t } = useAppTranslation();
return ( return (
<RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
<RdxPageHeader <RdxPageHeader
@ -33,5 +42,6 @@ export const SettingsLayout = ({ sections }: SettingsLayoutProps) => {
</div> </div>
</RdxPageStack> </RdxPageStack>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -1,5 +1,10 @@
import { ErrorAlert, PageErrorState, SimpleSearchInput } from "@erp/core/components"; import { ErrorAlert, PageErrorState, SimpleSearchInput } from "@erp/core/components";
import { RdxPageContainer, RdxPageHeader, RdxPageStack } from "@repo/rdx-ui/components"; import {
RdxPageContainer,
RdxPageFrame,
RdxPageHeader,
RdxPageStack,
} from "@repo/rdx-ui/components";
import { import {
Button, Button,
Spinner, Spinner,
@ -18,6 +23,7 @@ import { AuditFilterChips, AuditFiltersPanel } from "./audit-filters";
import { AuditGrid } from "./audit-grid"; import { AuditGrid } from "./audit-grid";
import { useAuditGridColumns } from "./use-audit-grid-columns"; import { useAuditGridColumns } from "./use-audit-grid-columns";
/** Compone el listado de auditoría dentro del frame estándar de página. */
export const AuditPage = () => { export const AuditPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const controller = useAuditPageController(); const controller = useAuditPageController();
@ -28,7 +34,8 @@ export const AuditPage = () => {
if (controller.isError && !controller.hasUsableData) { if (controller.isError && !controller.hasUsableData) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={t("audit.error.load")} description={t("audit.error.load")}
primaryAction={{ primaryAction={{
@ -40,10 +47,12 @@ export const AuditPage = () => {
variant="unknown" variant="unknown"
/> />
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
} }
return ( return (
<RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
<RdxPageHeader description={t("audit.description")} title={t("audit.title")} /> <RdxPageHeader description={t("audit.description")} title={t("audit.title")} />
@ -90,6 +99,7 @@ export const AuditPage = () => {
<AuditEntrySheet entryId={controller.entry} onClose={controller.closeEntry} /> <AuditEntrySheet entryId={controller.entry} onClose={controller.closeEntry} />
</RdxPageStack> </RdxPageStack>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -1,4 +1,5 @@
import { Card, CardContent, RdxBadge } from "@repo/shadcn-ui/components"; import { RdxBadge } from "@repo/rdx-ui/components";
import { Card, CardContent } from "@repo/shadcn-ui/components";
import { CalendarClockIcon } from "lucide-react"; import { CalendarClockIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";

View File

@ -1,6 +1,6 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { RdxPageContainer } from "@repo/rdx-ui/components"; import { RdxBadge, RdxPageContainer } from "@repo/rdx-ui/components";
import { Button, Card, CardContent, RdxBadge } from "@repo/shadcn-ui/components"; import { Button, Card, CardContent } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";

View File

@ -1,2 +1 @@
export * from './page-form-header'; export * from "./page-keyboard-shortcuts-button";
export * from './page-keyboard-shortcuts-button';

View File

@ -2,14 +2,6 @@ import type { ModuleClientParams } from "@erp/core/client";
import { lazy } from "react"; import { lazy } from "react";
import { Outlet, type RouteObject } from "react-router-dom"; import { Outlet, type RouteObject } from "react-router-dom";
const ProformaLayout = lazy(() =>
import("./proformas/shared/ui").then((m) => ({ default: m.ProformaLayout }))
);
const RecurringProformaLayout = lazy(() =>
import("./recurring-proformas/shared/ui").then((m) => ({ default: m.RecurringProformaLayout }))
);
const ProformasListPage = lazy(() => const ProformasListPage = lazy(() =>
import("./proformas/list").then((m) => ({ default: m.ListProformasPage })) import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
); );
@ -46,10 +38,6 @@ const RecurringProformaDetailPage = lazy(() =>
import("./recurring-proformas").then((m) => ({ default: m.RecurringProformaDetailPage })) import("./recurring-proformas").then((m) => ({ default: m.RecurringProformaDetailPage }))
); );
const IssuedInvoicesLayout = lazy(() =>
import("./issued-invoices/shared/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
);
const IssuedInvoiceListPage = lazy(() => const IssuedInvoiceListPage = lazy(() =>
import("./issued-invoices/list").then((m) => ({ default: m.ListIssuedInvoicesPage })) import("./issued-invoices/list").then((m) => ({ default: m.ListIssuedInvoicesPage }))
); );
@ -70,11 +58,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-sidebar", layout: "app-sidebar",
protected: true, protected: true,
}, },
element: ( element: <Outlet context={params} />,
<ProformaLayout>
<Outlet context={params} />
</ProformaLayout>
),
children: [ children: [
{ {
index: true, index: true,
@ -101,11 +85,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <ProformasCreatePage />,
<ProformaLayout>
<ProformasCreatePage />
</ProformaLayout>
),
}, },
{ {
@ -114,11 +94,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <ProformaUpdatePage />,
<ProformaLayout>
<ProformaUpdatePage />
</ProformaLayout>
),
}, },
{ {
@ -127,11 +103,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <ProformaDetailPage />,
<ProformaLayout>
<ProformaDetailPage />
</ProformaLayout>
),
}, },
{ {
path: "recurring-proformas", path: "recurring-proformas",
@ -139,11 +111,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-sidebar", layout: "app-sidebar",
protected: true, protected: true,
}, },
element: ( element: <RecurringProformasListPage />,
<RecurringProformaLayout>
<RecurringProformasListPage />
</RecurringProformaLayout>
),
}, },
{ {
path: "recurring-proformas/archived", path: "recurring-proformas/archived",
@ -151,11 +119,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-sidebar", layout: "app-sidebar",
protected: true, protected: true,
}, },
element: ( element: <RecurringProformasListPage />,
<RecurringProformaLayout>
<RecurringProformasListPage />
</RecurringProformaLayout>
),
}, },
{ {
path: "recurring-proformas/create", path: "recurring-proformas/create",
@ -163,11 +127,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <RecurringProformaCreatePage />,
<RecurringProformaLayout>
<RecurringProformaCreatePage />
</RecurringProformaLayout>
),
}, },
{ {
path: "recurring-proformas/:recurring_proforma_id", path: "recurring-proformas/:recurring_proforma_id",
@ -175,11 +135,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <RecurringProformaDetailPage />,
<RecurringProformaLayout>
<RecurringProformaDetailPage />
</RecurringProformaLayout>
),
}, },
{ {
@ -188,11 +144,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-sidebar", layout: "app-sidebar",
protected: true, protected: true,
}, },
element: ( element: <Outlet context={params} />,
<IssuedInvoicesLayout>
<Outlet context={params} />
</IssuedInvoicesLayout>
),
children: [ children: [
{ path: "", index: true, element: <IssuedInvoiceListPage /> }, // index { path: "", index: true, element: <IssuedInvoiceListPage /> }, // index
{ path: "list", element: <IssuedInvoiceListPage /> }, { path: "list", element: <IssuedInvoiceListPage /> },
@ -216,11 +168,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <IssuedInvoiceDetailPage />,
<IssuedInvoicesLayout>
<IssuedInvoiceDetailPage />
</IssuedInvoicesLayout>
),
}, },
{ {
path: "settings/billing/verifactu", path: "settings/billing/verifactu",

View File

@ -10,7 +10,7 @@ export const IssuedInvoiceDetailContent = ({
inspector, inspector,
}: IssuedInvoiceDetailContentProps) => { }: IssuedInvoiceDetailContentProps) => {
return ( return (
<div className="min-h-0 flex-1 overflow-auto lg:overflow-hidden"> <div className="min-h-full lg:h-full lg:min-h-0 lg:overflow-hidden">
<div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_340px] lg:overflow-hidden xl:grid-cols-[minmax(0,1fr)_380px]"> <div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_340px] lg:overflow-hidden xl:grid-cols-[minmax(0,1fr)_380px]">
{documentPane} {documentPane}
{inspector} {inspector}

View File

@ -1,4 +1,4 @@
import { PageFormHeader } from "@erp/core/components"; import { RdxEditorHeader } from "@repo/rdx-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@ -26,17 +26,17 @@ export const IssuedInvoicePageHeader = ({
contentClassName, contentClassName,
}: IssuedInvoicePageHeaderProps) => { }: IssuedInvoicePageHeaderProps) => {
return ( return (
<PageFormHeader <RdxEditorHeader
actions={actions} actions={actions}
backLabel={backLabel} backLabel={backLabel}
className={cn("shrink-0", className)} className={cn("shrink-0", className)}
contentClassName={contentClassName} contentClassName={contentClassName}
onBack={onBack} onBack={onBack}
showStatus={showStatus} showStatus={showStatus}
statusLabel={statusLabel} status={statusLabel}
title={title} title={title}
> >
{children} {children}
</PageFormHeader> </RdxEditorHeader>
); );
}; };

View File

@ -1,4 +1,5 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import { useIssuedInvoiceDetailPageController } from "../../controllers"; import { useIssuedInvoiceDetailPageController } from "../../controllers";
@ -20,8 +21,8 @@ export const IssuedInvoiceDetailPage = () => {
if (ctrl.state.pageError) { if (ctrl.state.pageError) {
return ( return (
<div className="p-4 sm:p-6"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={ctrl.state.pageError.description} description={ctrl.state.pageError.description}
primaryAction={ctrl.state.pageError.primaryAction} primaryAction={ctrl.state.pageError.primaryAction}
@ -30,15 +31,15 @@ export const IssuedInvoiceDetailPage = () => {
title={ctrl.state.pageError.title} title={ctrl.state.pageError.title}
variant={ctrl.state.pageError.variant} variant={ctrl.state.pageError.variant}
/> />
</div> </RdxPageContainer>
</div> </RdxPageFrame>
); );
} }
if (!(ctrl.header && ctrl.document && ctrl.general)) { if (!(ctrl.header && ctrl.document && ctrl.general)) {
return ( return (
<div className="p-4 sm:p-6"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={t("pages.issued_invoices.detail.page_error.inconsistent.description")} description={t("pages.issued_invoices.detail.page_error.inconsistent.description")}
secondaryAction={{ secondaryAction={{
@ -48,20 +49,23 @@ export const IssuedInvoiceDetailPage = () => {
title={t("pages.issued_invoices.detail.page_error.unknown.title")} title={t("pages.issued_invoices.detail.page_error.unknown.title")}
variant="unknown" variant="unknown"
/> />
</div> </RdxPageContainer>
</div> </RdxPageFrame>
); );
} }
return ( return (
<div className="flex h-full min-h-0 flex-col overflow-hidden"> <RdxPageFrame
contentClassName="lg:overflow-hidden"
header={
<IssuedInvoiceDetailHeader <IssuedInvoiceDetailHeader
actions={ctrl.actions} actions={ctrl.actions}
header={ctrl.header} header={ctrl.header}
onBack={ctrl.navigation.navigateBack} onBack={ctrl.navigation.navigateBack}
onExecuteAction={ctrl.handlers.executeAction} onExecuteAction={ctrl.handlers.executeAction}
/> />
}
>
<IssuedInvoiceDetailContent <IssuedInvoiceDetailContent
documentPane={ documentPane={
<IssuedInvoiceDocumentPane <IssuedInvoiceDocumentPane
@ -80,6 +84,6 @@ export const IssuedInvoiceDetailPage = () => {
/> />
} }
/> />
</div> </RdxPageFrame>
); );
}; };

View File

@ -1,3 +1,4 @@
import { RdxBadge } from "@repo/rdx-ui/components";
import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components"; import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { import {

View File

@ -1,3 +1,4 @@
import { RdxBadge } from "@repo/rdx-ui/components";
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components"; import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";

View File

@ -1,6 +1,6 @@
import { ErrorAlert, PageErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components"; import { ErrorAlert, PageErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components";
import { useReturnToNavigation } from "@erp/core/hooks"; import { useReturnToNavigation } from "@erp/core/hooks";
import { LogoVerifactu } from "@repo/rdx-ui/components"; import { LogoVerifactu, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components"; import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@ -58,8 +58,8 @@ export const ListIssuedInvoicesPage = () => {
if (hasBlockingError && pageError) { if (hasBlockingError && pageError) {
return ( return (
<div className="p-4 sm:p-6"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={pageError.description} description={pageError.description}
primaryAction={ primaryAction={
@ -88,13 +88,14 @@ export const ListIssuedInvoicesPage = () => {
title={pageError.title} title={pageError.title}
variant={pageError.variant} variant={pageError.variant}
/> />
</div> </RdxPageContainer>
</div> </RdxPageFrame>
); );
} }
return ( return (
<div className="p-6 space-y-6"> <RdxPageFrame>
<RdxPageContainer className="space-y-6" width="full">
<PageHeader <PageHeader
description={t("pages.issued_invoices.list.description")} description={t("pages.issued_invoices.list.description")}
rightSlot={ rightSlot={
@ -127,7 +128,6 @@ export const ListIssuedInvoicesPage = () => {
<LogoVerifactu className="text-green-800 w-50" color="" /> <LogoVerifactu className="text-green-800 w-50" color="" />
</Alert> </Alert>
<div className="min-h-0 flex-1 overflow-auto">
<IssuedInvoicesGrid <IssuedInvoicesGrid
columns={columns} columns={columns}
data={listCtrl.data} data={listCtrl.data}
@ -164,10 +164,10 @@ export const ListIssuedInvoicesPage = () => {
</div> </div>
} }
/> />
</div>
{listCtrl.data.totalItems === 0 ? <IssuedInvoicesEmptyGrid /> : null} {listCtrl.data.totalItems === 0 ? <IssuedInvoicesEmptyGrid /> : null}
</div> </div>
</div> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -1 +0,0 @@
export * from "./issued-invoices-layout";

View File

@ -1,5 +0,0 @@
import type { PropsWithChildren } from "react";
export const IssuedInvoicesLayout = ({ children }: PropsWithChildren) => {
return <div className="space-y-4">{children}</div>;
};

View File

@ -38,7 +38,7 @@ export const ProformaCreateCustomerField = ({
<div className="relative"> <div className="relative">
<Button <Button
className={cn( className={cn(
"h-10 w-full justify-between border bg-muted/50 px-3 font-normal text-left text-foreground hover:bg-muted/50", "h-10 w-full justify-between border bg-muted/20 px-3 font-normal text-left text-foreground hover:bg-muted/50",
!selectedCustomer && "text-muted-foreground" !selectedCustomer && "text-muted-foreground"
)} )}
disabled={isDisabled} disabled={isDisabled}

View File

@ -22,7 +22,7 @@ export const ProformaCreateHeader = ({
actions={ actions={
<FormActionsBar align="end" reverseOnMobile={false}> <FormActionsBar align="end" reverseOnMobile={false}>
<PageKeyboardShortcutsButton <PageKeyboardShortcutsButton
className="hidden sm:flex" className="hidden"
label="Ver atajos de teclado" label="Ver atajos de teclado"
shortcuts={[ shortcuts={[
{ keys: "Ctrl+S", label: "Crear y continuar" }, { keys: "Ctrl+S", label: "Crear y continuar" },

View File

@ -1,8 +1,8 @@
import type { CustomerSelectionOption } from "@erp/customers"; import type { CustomerSelectionOption } from "@erp/customers";
import { import {
DatePickerField, DatePickerField,
FormSectionCard, RdxFormGrid,
FormSectionGrid, RdxPanel,
SelectField, SelectField,
type SelectFieldItem, type SelectFieldItem,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
@ -43,13 +43,14 @@ export const ProformaCreateInitialInfoCard = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<FormSectionCard <RdxPanel
icon={<FileTextIcon className="size-5" />} icon={<FileTextIcon className="size-5" />}
title={t("proformas.create.initial_info.title", "Información inicial")} title={t("proformas.create.initial_info.title", "Información inicial")}
variant="card"
> >
<FormSectionGrid className="md:grid-cols-6 2xl:grid-cols-12"> <RdxFormGrid className="md:grid-cols-6 2xl:grid-cols-12">
<ProformaCreateCustomerField <ProformaCreateCustomerField
className="md:col-span-4" className="md:col-span-8"
disabled={disabled} disabled={disabled}
onClearCustomer={onClearCustomer} onClearCustomer={onClearCustomer}
onSelectCustomer={onSelectCustomer} onSelectCustomer={onSelectCustomer}
@ -57,7 +58,7 @@ export const ProformaCreateInitialInfoCard = ({
/> />
<DatePickerField<ProformaCreateForm> <DatePickerField<ProformaCreateForm>
className="md:col-span-2" className="md:col-span-4"
disabled={disabled} disabled={disabled}
label={t("proformas.create.initial_info.proforma_date.label", "Fecha")} label={t("proformas.create.initial_info.proforma_date.label", "Fecha")}
name="proformaDate" name="proformaDate"
@ -69,7 +70,7 @@ export const ProformaCreateInitialInfoCard = ({
/> />
<SelectField<ProformaCreateForm> <SelectField<ProformaCreateForm>
className="md:col-span-2" className="md:col-span-3"
disabled={disabled || isProformaSeriesLoading} disabled={disabled || isProformaSeriesLoading}
items={proformaSeriesOptions} items={proformaSeriesOptions}
label={t("proformas.create.initial_info.proforma_series.label", "Serie de esta proforma")} label={t("proformas.create.initial_info.proforma_series.label", "Serie de esta proforma")}
@ -87,7 +88,7 @@ export const ProformaCreateInitialInfoCard = ({
/> />
<SelectField<ProformaCreateForm> <SelectField<ProformaCreateForm>
className="md:col-span-2" className="md:col-span-3"
disabled={disabled || isTargetInvoiceSeriesLoading} disabled={disabled || isTargetInvoiceSeriesLoading}
items={targetInvoiceSeriesOptions} items={targetInvoiceSeriesOptions}
label={t("proformas.create.initial_info.invoice_series.label", "Serie de la factura")} label={t("proformas.create.initial_info.invoice_series.label", "Serie de la factura")}
@ -100,7 +101,7 @@ export const ProformaCreateInitialInfoCard = ({
/> />
<SelectField<ProformaCreateForm> <SelectField<ProformaCreateForm>
className="md:col-span-4" className="md:col-span-6"
clearable clearable
clearLabel={t("form_fields.proformas.payment_term.clear_label", "Sin plazo de pago")} clearLabel={t("form_fields.proformas.payment_term.clear_label", "Sin plazo de pago")}
disabled={disabled || isPaymentTermsLoading} disabled={disabled || isPaymentTermsLoading}
@ -115,7 +116,7 @@ export const ProformaCreateInitialInfoCard = ({
: t("form_fields.proformas.payment_term.placeholder", "Selecciona un plazo de pago") : t("form_fields.proformas.payment_term.placeholder", "Selecciona un plazo de pago")
} }
/> />
</FormSectionGrid> </RdxFormGrid>
</FormSectionCard> </RdxPanel>
); );
}; };

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

View File

@ -1,7 +1,12 @@
import { ErrorAlert } from "@erp/core/components"; import { ErrorAlert } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers"; import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers";
import { RdxEditorPageLayout, RdxPageContainer, RdxPageStack } from "@repo/rdx-ui/components"; import {
RdxEditorLayout,
RdxPageContainer,
RdxPageFrame,
RdxPageStack,
} from "@repo/rdx-ui/components";
import { FormProvider } from "react-hook-form"; import { FormProvider } from "react-hook-form";
import { ProformaInfoAlert } from "../../../update/ui/components"; import { ProformaInfoAlert } from "../../../update/ui/components";
@ -16,9 +21,11 @@ export const ProformaCreatePage = () => {
}); });
return ( return (
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none">
<FormProvider {...createCtrl.form}> <FormProvider {...createCtrl.form}>
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}> <UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
<RdxPageFrame
contentClassName="bg-muted/20"
header={
<ProformaCreateHeader <ProformaCreateHeader
formId={createCtrl.formId} formId={createCtrl.formId}
isSaving={createCtrl.isCreating} isSaving={createCtrl.isCreating}
@ -29,13 +36,10 @@ export const ProformaCreatePage = () => {
fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente. fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente.
</ProformaInfoAlert> </ProformaInfoAlert>
</ProformaCreateHeader> </ProformaCreateHeader>
}
<RdxPageContainer
className="min-h-0 flex-1 overflow-y-auto bg-muted/20"
padding="none"
width="full"
> >
<RdxEditorPageLayout width="simple"> <RdxPageContainer width="content">
<RdxEditorLayout>
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
{createCtrl.isCreateError && ( {createCtrl.isCreateError && (
<ErrorAlert <ErrorAlert
@ -75,8 +79,9 @@ export const ProformaCreatePage = () => {
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions} targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
/> />
</RdxPageStack> </RdxPageStack>
</RdxEditorPageLayout> </RdxEditorLayout>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
<SelectCustomerDialog <SelectCustomerDialog
ctrl={selectCustomerCtrl.selectCtrl} ctrl={selectCustomerCtrl.selectCtrl}
@ -85,6 +90,5 @@ export const ProformaCreatePage = () => {
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} /> <QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
</UnsavedChangesProvider> </UnsavedChangesProvider>
</FormProvider> </FormProvider>
</RdxPageStack>
); );
}; };

View File

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

View File

@ -7,7 +7,7 @@ type ProformaDetailContentProps = {
export const ProformaDetailContent = ({ documentPane, inspector }: ProformaDetailContentProps) => { export const ProformaDetailContent = ({ documentPane, inspector }: ProformaDetailContentProps) => {
return ( return (
<div className="min-h-0 flex-1 overflow-auto lg:overflow-hidden "> <div className="min-h-full lg:h-full lg:min-h-0 lg:overflow-hidden">
<div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:overflow-hidden lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_380px]"> <div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:overflow-hidden lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_380px]">
{documentPane} {documentPane}
{inspector} {inspector}

View File

@ -1,5 +1,5 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { RdxPageStack } from "@repo/rdx-ui/components"; import { RdxPageFrame } from "@repo/rdx-ui/components";
import { ChangeProformaStatusDialog } from "../../../change-status"; import { ChangeProformaStatusDialog } from "../../../change-status";
import { IssueProformaDialog } from "../../../issue-proforma"; import { IssueProformaDialog } from "../../../issue-proforma";
@ -41,14 +41,17 @@ export const ProformaDetailPage = () => {
} }
return ( return (
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none"> <RdxPageFrame
contentClassName="lg:overflow-hidden"
header={
<ProformaDetailHeader <ProformaDetailHeader
actions={ctrl.actions} actions={ctrl.actions}
header={ctrl.header} header={ctrl.header}
onBack={ctrl.navigation.navigateBack} onBack={ctrl.navigation.navigateBack}
onExecuteAction={ctrl.handlers.executeAction} onExecuteAction={ctrl.handlers.executeAction}
/> />
}
>
<ProformaDetailContent <ProformaDetailContent
documentPane={ documentPane={
<ProformaDocumentPane <ProformaDocumentPane
@ -114,6 +117,6 @@ export const ProformaDetailPage = () => {
open={ctrl.dialogs.createRecurring.open} open={ctrl.dialogs.createRecurring.open}
unit={ctrl.dialogs.createRecurring.unit} unit={ctrl.dialogs.createRecurring.unit}
/> />
</RdxPageStack> </RdxPageFrame>
); );
}; };

View File

@ -1,13 +1,14 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; 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 { 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 type { ProformaListRow } from "../../../../shared";
import { PROFORMA_STATUS } from "../../../../shared"; import { PROFORMA_STATUS } from "../../../../shared";
import { useProformasGridColumns } from "./use-proforma-grid-columns"; import { useProformasGridColumns } from "./use-proforma-grid-columns";
const proformaFixture: ProformaListRow = { const proformaFixture: ProformaListRow = {
@ -104,12 +105,16 @@ test("mantiene las destructivas en overflow y conserva disabled", () => {
assert.equal(viewAction?.disabled, true); 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 didView = false;
let didEdit = false; let didEdit = false;
let didCreateRecurring = false;
let didDelete = false; let didDelete = false;
const props = getRowActionsProps("normal", { const props = getRowActionsProps("normal", {
onCreateRecurringClick: () => {
didCreateRecurring = true;
},
onDeleteClick: () => { onDeleteClick: () => {
didDelete = true; 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 === "view")?.onSelect();
props.primaryActions?.find((action) => action.id === "edit")?.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(); props.overflowActions?.find((action) => action.id === "delete")?.onSelect();
assert.equal(didView, true); assert.equal(didView, true);
assert.equal(didEdit, true); assert.equal(didEdit, true);
assert.equal(didCreateRecurring, true);
assert.equal(didDelete, 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 { 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 { DateHelper } from "@repo/rdx-utils";
import { import { Checkbox, Spinner } from "@repo/shadcn-ui/components";
Checkbox,
Spinner,
} from "@repo/shadcn-ui/components";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { import {
CalendarSyncIcon,
CopyIcon, CopyIcon,
ExternalLinkIcon, ExternalLinkIcon,
EyeIcon, EyeIcon,
@ -37,6 +35,7 @@ type GridActionHandlers = {
onIssueClick?: (proforma: ProformaListRow) => void; onIssueClick?: (proforma: ProformaListRow) => void;
onArchiveClick?: (proforma: ProformaListRow) => void; onArchiveClick?: (proforma: ProformaListRow) => void;
onDuplicateClick?: (proforma: ProformaListRow) => void; onDuplicateClick?: (proforma: ProformaListRow) => void;
onCreateRecurringClick?: (proforma: ProformaListRow) => void;
onUnarchiveClick?: (proforma: ProformaListRow) => void; onUnarchiveClick?: (proforma: ProformaListRow) => void;
onDeleteClick?: (proforma: ProformaListRow) => void; onDeleteClick?: (proforma: ProformaListRow) => void;
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void; onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
@ -52,10 +51,8 @@ type GridActionHandlers = {
isPdfDownloading?: boolean; isPdfDownloading?: boolean;
}; };
const renderActionIcon = ( const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
Icon: ComponentType<{ className?: string }>, loading ? <Spinner className="size-4" /> : <Icon className="size-4" />;
loading?: boolean
) => (loading ? <Spinner className="size-4" /> : <Icon className="size-4" />);
export function useProformasGridColumns( export function useProformasGridColumns(
scope: ProformaListScope, scope: ProformaListScope,
@ -312,6 +309,7 @@ export function useProformasGridColumns(
const handleView = () => actionHandlers.onViewClick?.(proforma); const handleView = () => actionHandlers.onViewClick?.(proforma);
const handleEdit = () => actionHandlers.onEditClick?.(proforma); const handleEdit = () => actionHandlers.onEditClick?.(proforma);
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma); const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma); const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma); const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
const handleIssue = () => actionHandlers.onIssueClick?.(proforma); const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
@ -416,6 +414,14 @@ export function useProformasGridColumns(
: undefined, : undefined,
onSelect: handleDuplicate, 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", id: "archive",
label: t("pages.proformas.list.actions.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 { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
import { DeleteProformaDialog } from "../../../delete"; import { DeleteProformaDialog } from "../../../delete";
import { prepareDeleteProformaTargets } from "../../../delete/utils"; import { prepareDeleteProformaTargets } from "../../../delete/utils";
import { useCreateRecurringProformaFromProformaDialogController } from "../../../detail/controllers";
import { CreateRecurringProformaFromProformaDialog } from "../../../detail/ui/components";
import { downloadProformaPDFApi } from "../../../download-pdf/api"; import { downloadProformaPDFApi } from "../../../download-pdf/api";
import { IssueProformaDialog } from "../../../issue-proforma"; import { IssueProformaDialog } from "../../../issue-proforma";
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils"; import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
@ -238,6 +240,10 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
goHome, goHome,
isRetrying, isRetrying,
} = useListProformasPageController(scope); } = useListProformasPageController(scope);
const createRecurringDialogCtrl = useCreateRecurringProformaFromProformaDialogController({
proformaId: "",
proformaReference: null,
});
const handleEditClick = (proformaId: string) => { const handleEditClick = (proformaId: string) => {
navigate({ navigate({
@ -293,6 +299,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
onViewClick: (proforma) => handleOpenDetailClick(proforma.id), onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
onEditClick: (proforma) => handleEditClick(proforma.id), onEditClick: (proforma) => handleEditClick(proforma.id),
onDuplicateClick: handleDuplicateClick, onDuplicateClick: handleDuplicateClick,
onCreateRecurringClick: (proforma) =>
createRecurringDialogCtrl.openDialog({
proformaId: proforma.id,
proformaReference: proforma.proformaReference,
}),
onIssueClick: (proformaRow) => onIssueClick: (proformaRow) =>
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)), issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
onArchiveClick: archiveDialogCtrl.openDialog, onArchiveClick: archiveDialogCtrl.openDialog,
@ -550,6 +561,21 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
targets={changeStatusDialogCtrl.targets} 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 <DeleteProformaDialog
isSubmitting={deleteDialogCtrl.isSubmitting} isSubmitting={deleteDialogCtrl.isSubmitting}
onConfirm={deleteDialogCtrl.confirmDelete} onConfirm={deleteDialogCtrl.confirmDelete}

View File

@ -2,6 +2,7 @@ import { ErrorAlert } from "@erp/core/components";
import { import {
RdxPageActions, RdxPageActions,
RdxPageContainer, RdxPageContainer,
RdxPageFrame,
RdxPageHeader, RdxPageHeader,
RdxPageStack, RdxPageStack,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
@ -30,6 +31,7 @@ export const ScopedListProformasPageView = ({
dialogs, dialogs,
}: ScopedListProformasPageViewProps) => { }: ScopedListProformasPageViewProps) => {
return ( return (
<RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
<RdxPageHeader <RdxPageHeader
@ -47,5 +49,6 @@ export const ScopedListProformasPageView = ({
{dialogs} {dialogs}
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -1,3 +1,2 @@
export * from "./proforma-layout";
export * from "./proforma-issuance-readiness-alert"; export * from "./proforma-issuance-readiness-alert";
export * from "./proforma-page-header"; export * from "./proforma-page-header";

View File

@ -1,9 +0,0 @@
import type { PropsWithChildren } from "react";
export function ProformaLayout({ children }: PropsWithChildren) {
return (
<div className="flex flex-col h-full w-full" id="proformas ">
{children}
</div>
);
}

View File

@ -1,4 +1,4 @@
import { RdxPageHeader } from "@repo/rdx-ui/components"; import { RdxEditorHeader } from "@repo/rdx-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@ -26,18 +26,17 @@ export const ProformaPageHeader = ({
contentClassName, contentClassName,
}: ProformaPageHeaderProps) => { }: ProformaPageHeaderProps) => {
return ( return (
<RdxPageHeader <RdxEditorHeader
actions={actions} actions={actions}
backLabel={backLabel} backLabel={backLabel}
className={cn("shrink-0", className)} className={cn("shrink-0", className)}
contentClassName={contentClassName} contentClassName={contentClassName}
onBack={onBack} onBack={onBack}
showStatus={showStatus} showStatus={showStatus}
statusLabel={statusLabel} status={statusLabel}
sticky
title={title} title={title}
> >
{children} {children}
</RdxPageHeader> </RdxEditorHeader>
); );
}; };

View File

@ -1,5 +1,5 @@
import type { CustomerSelectionOption } from "@erp/customers"; import type { CustomerSelectionOption } from "@erp/customers";
import { RdxEditorPageLayout, type SelectFieldItem } from "@repo/rdx-ui/components"; import { RdxEditorLayout, type SelectFieldItem } from "@repo/rdx-ui/components";
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers"; import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
import type { SubmitEventHandler } from "react"; import type { SubmitEventHandler } from "react";
@ -53,7 +53,7 @@ export const ProformaUpdateEditorForm = ({
className, className,
}: ProformaUpdateEditorProps) => { }: ProformaUpdateEditorProps) => {
return ( return (
<RdxEditorPageLayout <RdxEditorLayout
aside={ aside={
<div className="flex min-w-0 flex-col gap-6"> <div className="flex min-w-0 flex-col gap-6">
<ProformaUpdateRecipientEditor <ProformaUpdateRecipientEditor
@ -75,7 +75,6 @@ export const ProformaUpdateEditorForm = ({
</div> </div>
} }
className={className} className={className}
width="full"
> >
<div className="min-w-0"> <div className="min-w-0">
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}> <form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
@ -91,6 +90,6 @@ export const ProformaUpdateEditorForm = ({
</div> </div>
</form> </form>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
); );
}; };

View File

@ -52,6 +52,21 @@ test("conecta la alta rápida directa y la alta rápida desde selector con callb
PercentageField: () => null, PercentageField: () => null,
RdxEditorStickyFooter: () => null, RdxEditorStickyFooter: () => null,
RdxPageContainer: ({ children }: { children: ReactNode }) => children, RdxPageContainer: ({ children }: { children: ReactNode }) => children,
RdxPageFrame: ({
children,
footer,
header,
}: {
children: ReactNode;
footer?: ReactNode;
header?: ReactNode;
}) => (
<>
{header}
{children}
{footer}
</>
),
RdxPageStack: ({ children }: { children: ReactNode }) => children, RdxPageStack: ({ children }: { children: ReactNode }) => children,
RhfSubmitActionButton: () => null, RhfSubmitActionButton: () => null,
}, },

View File

@ -7,6 +7,7 @@ import {
RdxBadge, RdxBadge,
RdxEditorActionBar, RdxEditorActionBar,
RdxPageContainer, RdxPageContainer,
RdxPageFrame,
RdxPageStack, RdxPageStack,
RhfSubmitActionButton, RhfSubmitActionButton,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
@ -94,11 +95,53 @@ export const ProformaUpdatePage = () => {
} }
const hasPageAlerts = updateCtrl.isUpdateError || updateCtrl.isSeriesLoadError; const hasPageAlerts = updateCtrl.isUpdateError || updateCtrl.isSeriesLoadError;
const actionBar = (
<RdxEditorActionBar
leading={
<ProformaCompactTotals
currency={updateCtrl.currencyCode}
globalDiscountField={
<PercentageField
className="md:col-span-4 md:col-start-1"
disabled={updateCtrl.isUpdating}
inputClassName="bg-background"
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
name="globalDiscountPercentage"
/>
}
showRec={updateCtrl.taxCtrl.hasRecPercentage}
showRetention={updateCtrl.taxCtrl.hasRetentionPercentage}
totals={updateCtrl.totalsCtrl.totals}
variant="embedded"
/>
}
primary={
<RhfSubmitActionButton
busyLabel="Guardando..."
disabled={updateCtrl.isUpdating}
formId={updateCtrl.formId}
isBusy={updateCtrl.form.formState.isSubmitting}
label="Guardar"
/>
}
secondary={
<CancelActionButton
disabled={updateCtrl.isUpdating}
label="Cancelar"
onCancel={handleCancel}
variant="outline"
/>
}
/>
);
return ( return (
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none">
<FormProvider {...updateCtrl.form}> <FormProvider {...updateCtrl.form}>
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}> <UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
<RdxPageFrame
contentClassName="bg-muted/20"
footer={actionBar}
header={
<ProformaUpdateHeader <ProformaUpdateHeader
formId={updateCtrl.formId} formId={updateCtrl.formId}
hasChanges={updateCtrl.form.formState.isDirty} hasChanges={updateCtrl.form.formState.isDirty}
@ -140,13 +183,9 @@ export const ProformaUpdatePage = () => {
//onDuplicate={handleDuplicate} //onDuplicate={handleDuplicate}
//onExportPdf={handleExportPdf} //onExportPdf={handleExportPdf}
/> />
}
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
<RdxPageContainer
className="min-h-0 flex-1 overflow-y-auto"
padding="default"
width="full"
> >
<RdxPageContainer padding="default" width="full">
{hasPageAlerts ? ( {hasPageAlerts ? (
<RdxPageContainer className="pb-0" width="full"> <RdxPageContainer className="pb-0" width="full">
<RdxPageStack spacing="compact"> <RdxPageStack spacing="compact">
@ -203,48 +242,7 @@ export const ProformaUpdatePage = () => {
totalsCtrl={updateCtrl.totalsCtrl} totalsCtrl={updateCtrl.totalsCtrl}
/> />
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
<RdxEditorActionBar
leading={
<ProformaCompactTotals
currency={updateCtrl.currencyCode}
globalDiscountField={
<PercentageField
className="md:col-span-4 md:col-start-1"
disabled={updateCtrl.isUpdating}
inputClassName="bg-background"
label={t(
"proformas.update.totals.globalDiscountPercentage",
"Descuento global"
)}
name="globalDiscountPercentage"
/>
}
showRec={updateCtrl.taxCtrl.hasRecPercentage}
showRetention={updateCtrl.taxCtrl.hasRetentionPercentage}
totals={updateCtrl.totalsCtrl.totals}
variant="embedded"
/>
}
primary={
<RhfSubmitActionButton
busyLabel="Guardando..."
disabled={updateCtrl.isUpdating}
formId={updateCtrl.formId}
isBusy={updateCtrl.form.formState.isSubmitting}
label="Guardar"
/>
}
secondary={
<CancelActionButton
disabled={updateCtrl.isUpdating}
label="Cancelar"
onCancel={handleCancel}
variant="outline"
/>
}
/>
</div>
<SelectCustomerDialog <SelectCustomerDialog
ctrl={selectCustomerCtrl.selectCtrl} ctrl={selectCustomerCtrl.selectCtrl}
@ -266,6 +264,5 @@ export const ProformaUpdatePage = () => {
/> />
</UnsavedChangesProvider> </UnsavedChangesProvider>
</FormProvider> </FormProvider>
</RdxPageStack>
); );
}; };

View File

@ -1,6 +1,10 @@
import { PageFormHeader } from "@erp/core/components";
import { useHookForm } from "@erp/core/hooks"; import { useHookForm } from "@erp/core/hooks";
import { RdxEditorPageLayout } from "@repo/rdx-ui/components"; import {
RdxEditorHeader,
RdxEditorLayout,
RdxPageContainer,
RdxPageFrame,
} from "@repo/rdx-ui/components";
import { import {
Button, Button,
Card, Card,
@ -50,8 +54,10 @@ export const RecurringProformaCreatePage = () => {
}); });
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden"> <RdxPageFrame
<PageFormHeader contentClassName="bg-muted/20"
header={
<RdxEditorHeader
actions={ actions={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
@ -77,8 +83,10 @@ export const RecurringProformaCreatePage = () => {
onBack={() => navigate("/recurring-proformas")} onBack={() => navigate("/recurring-proformas")}
title={t("recurring_proformas.create.title")} title={t("recurring_proformas.create.title")}
/> />
<div className="flex-1 overflow-y-auto bg-muted/20"> }
<RdxEditorPageLayout width="simple"> >
<RdxPageContainer width="content">
<RdxEditorLayout>
<form <form
className="space-y-6" className="space-y-6"
id={RECURRING_PROFORMA_CREATE_FORM_ID} id={RECURRING_PROFORMA_CREATE_FORM_ID}
@ -129,8 +137,8 @@ export const RecurringProformaCreatePage = () => {
</CardContent> </CardContent>
</Card> </Card>
</form> </form>
</RdxEditorPageLayout> </RdxEditorLayout>
</div> </RdxPageContainer>
</div> </RdxPageFrame>
); );
}; };

View File

@ -1,10 +1,5 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { import { RdxBadge, RdxPageContainer, RdxPageFrame, RdxPageStack } from "@repo/rdx-ui/components";
RdxBadge,
RdxEditorPageLayout,
RdxPageContainer,
RdxPageStack,
} from "@repo/rdx-ui/components";
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components"; import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
import { ArrowLeftIcon } from "lucide-react"; import { ArrowLeftIcon } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
@ -96,7 +91,8 @@ export const RecurringProformaDetailPage = () => {
}); });
return ( return (
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none"> <RdxPageFrame
header={
<ProformaPageHeader <ProformaPageHeader
actions={ actions={
<RecurringProformaDetailHeaderActions <RecurringProformaDetailHeaderActions
@ -131,14 +127,14 @@ export const RecurringProformaDetailPage = () => {
</div> </div>
} }
/> />
}
<RdxPageContainer className="min-h-0 flex-1 overflow-y-auto" padding="none" width="wide"> >
<RdxEditorPageLayout width="simple"> <RdxPageContainer width="wide">
<RdxPageStack spacing="default">
<RecurringProformaOperationalSummary <RecurringProformaOperationalSummary
locale={dateLocale} locale={dateLocale}
recurringProforma={recurringProforma} recurringProforma={recurringProforma}
/> />
</RdxEditorPageLayout>
<div className="grid gap-6 xl:grid-cols-2"> <div className="grid gap-6 xl:grid-cols-2">
<Card> <Card>
@ -268,7 +264,8 @@ export const RecurringProformaDetailPage = () => {
<ArrowLeftIcon aria-hidden className="size-4" /> <ArrowLeftIcon aria-hidden className="size-4" />
{t("recurring_proformas.detail.back")} {t("recurring_proformas.detail.back")}
</Button> </Button>
</RdxPageContainer>
</RdxPageStack> </RdxPageStack>
</RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -124,6 +124,7 @@ mock.module("./controllers/use-list-recurring-proformas-page-controller", {
mock.module("@repo/rdx-ui/components", { mock.module("@repo/rdx-ui/components", {
namedExports: { namedExports: {
RdxPageContainer: ({ children }: { children: ReactNode }) => <div>{children}</div>, RdxPageContainer: ({ children }: { children: ReactNode }) => <div>{children}</div>,
RdxPageFrame: ({ children }: { children: ReactNode }) => <div>{children}</div>,
RdxPageStack: ({ children }: { children: ReactNode }) => <div>{children}</div>, RdxPageStack: ({ children }: { children: ReactNode }) => <div>{children}</div>,
RdxPageHeader: ({ RdxPageHeader: ({
title, title,

View File

@ -16,6 +16,7 @@ import {
CircleIcon, CircleIcon,
CirclePauseIcon, CirclePauseIcon,
CopyIcon, CopyIcon,
PencilIcon,
PlayCircleIcon, PlayCircleIcon,
Trash2Icon, Trash2Icon,
} from "lucide-react"; } from "lucide-react";
@ -63,6 +64,7 @@ const actionIcons = {
complete: CheckCircle2Icon, complete: CheckCircle2Icon,
archive: ArchiveIcon, archive: ArchiveIcon,
duplicate: CopyIcon, duplicate: CopyIcon,
edit: PencilIcon,
delete: Trash2Icon, delete: Trash2Icon,
} as const; } as const;

View File

@ -2,6 +2,7 @@ import { ErrorAlert } from "@erp/core/components";
import { import {
RdxPageActions, RdxPageActions,
RdxPageContainer, RdxPageContainer,
RdxPageFrame,
RdxPageHeader, RdxPageHeader,
RdxPageStack, RdxPageStack,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
@ -28,6 +29,7 @@ export const ScopedListRecurringProformasPageView = ({
content, content,
}: ScopedListRecurringProformasPageViewProps) => { }: ScopedListRecurringProformasPageViewProps) => {
return ( return (
<RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
<RdxPageHeader <RdxPageHeader
@ -43,5 +45,6 @@ export const ScopedListRecurringProformasPageView = ({
{content} {content}
</RdxPageStack> </RdxPageStack>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -1 +0,0 @@
export * from "./recurring-proforma-layout";

View File

@ -1,9 +0,0 @@
import type { PropsWithChildren } from "react";
export function RecurringProformaLayout({ children }: PropsWithChildren) {
return (
<div className="flex flex-col h-full w-full" id="recurring_proformas ">
{children}
</div>
);
}

View File

@ -1,9 +1,10 @@
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components"; import { PageKeyboardShortcutsButton } from "@erp/core/components";
import { import {
CancelActionButton, CancelActionButton,
FormActionsBar, FormActionsBar,
type FormSecondaryAction, type FormSecondaryAction,
FormSecondaryActionsMenu, FormSecondaryActionsMenu,
RdxEditorHeader,
RhfSubmitActionButton, RhfSubmitActionButton,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@ -74,7 +75,7 @@ export const CustomerCreateHeader = ({
]; ];
return ( return (
<PageFormHeader <RdxEditorHeader
actions={ actions={
<FormActionsBar align="end" reverseOnMobile={false}> <FormActionsBar align="end" reverseOnMobile={false}>
<PageKeyboardShortcutsButton <PageKeyboardShortcutsButton
@ -113,10 +114,10 @@ export const CustomerCreateHeader = ({
className={className} className={className}
onBack={onCancel} onBack={onCancel}
showStatus={hasChanges} showStatus={hasChanges}
statusLabel={labels.modified} status={labels.modified}
title={labels.title} title={labels.title}
> >
{children} {children}
</PageFormHeader> </RdxEditorHeader>
); );
}; };

View File

@ -1,4 +1,4 @@
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { BackHistoryButton, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
@ -6,8 +6,8 @@ import { useTranslation } from "../../../i18n";
export const CustomerEditorSkeleton = () => { export const CustomerEditorSkeleton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <RdxPageFrame>
<AppContent> <RdxPageContainer width="content">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div aria-hidden="true" className="space-y-2"> <div aria-hidden="true" className="space-y-2">
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" /> <div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
@ -26,7 +26,7 @@ export const CustomerEditorSkeleton = () => {
<div className="h-28 w-full rounded-md bg-muted animate-pulse" /> <div className="h-28 w-full rounded-md bg-muted animate-pulse" />
</div> </div>
<span className="sr-only">{t("update.loading")}</span> <span className="sr-only">{t("update.loading")}</span>
</AppContent> </RdxPageContainer>
</> </RdxPageFrame>
); );
}; };

View File

@ -1,6 +1,6 @@
import { ErrorAlert } from "@erp/core/components"; import { ErrorAlert } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import { RdxEditorPageLayout } from "@repo/rdx-ui/components"; import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { FormProvider } from "react-hook-form"; import { FormProvider } from "react-hook-form";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
@ -19,9 +19,11 @@ export const CustomerCreatePage = () => {
const handleCancel = () => navigateBack(); const handleCancel = () => navigateBack();
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden">
<FormProvider {...createCtrl.form}> <FormProvider {...createCtrl.form}>
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}> <UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
<RdxPageFrame
contentClassName="bg-muted/20"
header={
<CustomerCreateHeader <CustomerCreateHeader
formId={createCtrl.formId} formId={createCtrl.formId}
hasChanges={createCtrl.form.formState.isDirty} hasChanges={createCtrl.form.formState.isDirty}
@ -42,9 +44,10 @@ export const CustomerCreatePage = () => {
//onDuplicate={handleDuplicate} //onDuplicate={handleDuplicate}
//onExportPdf={handleExportPdf} //onExportPdf={handleExportPdf}
/> />
}
<div className="flex-1 overflow-y-auto bg-muted/20"> >
<RdxEditorPageLayout width="simple"> <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="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="flex min-w-0 flex-col gap-6">
{createCtrl.isCreateError ? ( {createCtrl.isCreateError ? (
@ -65,10 +68,10 @@ export const CustomerCreatePage = () => {
/> />
</div> </div>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</div> </RdxPageContainer>
</RdxPageFrame>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</FormProvider> </FormProvider>
</div>
); );
}; };

View File

@ -2,10 +2,6 @@ import type { ModuleClientParams } from "@erp/core/client";
import { lazy } from "react"; import { lazy } from "react";
import { Outlet, type RouteObject } from "react-router-dom"; import { Outlet, type RouteObject } from "react-router-dom";
// Lazy load components
const CustomerLayout = lazy(() =>
import("./shared/ui").then((m) => ({ default: m.CustomerLayout }))
);
const CustomersListPage = lazy(() => const CustomersListPage = lazy(() =>
import("./list").then((m) => ({ default: m.ListCustomersPage })) import("./list").then((m) => ({ default: m.ListCustomersPage }))
); );
@ -28,11 +24,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
layout: "app-sidebar", layout: "app-sidebar",
protected: true, protected: true,
}, },
element: ( element: <Outlet context={params} />,
<CustomerLayout>
<Outlet context={params} />
</CustomerLayout>
),
children: [ children: [
{ {
index: true, index: true,
@ -51,11 +43,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <CustomerCreatePage />,
<CustomerLayout>
<CustomerCreatePage />
</CustomerLayout>
),
}, },
{ {
@ -64,11 +52,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <CustomerUpdatePage />,
<CustomerLayout>
<CustomerUpdatePage />
</CustomerLayout>
),
}, },
{ {
@ -77,11 +61,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
layout: "app-fullscreen", layout: "app-fullscreen",
protected: true, protected: true,
}, },
element: ( element: <CustomerViewPage />,
<CustomerLayout>
<CustomerViewPage />
</CustomerLayout>
),
}, },
/* /*

View File

@ -2,6 +2,7 @@ import { ErrorAlert } from "@erp/core/components";
import { import {
RdxPageActions, RdxPageActions,
RdxPageContainer, RdxPageContainer,
RdxPageFrame,
RdxPageHeader, RdxPageHeader,
RdxPageStack, RdxPageStack,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
@ -30,6 +31,7 @@ export const ListCustomersPageView = ({
dialogs, dialogs,
}: ListCustomersPageViewProps) => { }: ListCustomersPageViewProps) => {
return ( return (
<RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
<RdxPageHeader <RdxPageHeader
@ -47,5 +49,6 @@ export const ListCustomersPageView = ({
{dialogs} {dialogs}
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -1,6 +1,6 @@
import { PageErrorState, SimpleSearchInput } from "@erp/core/components"; import { PageErrorState, SimpleSearchInput } from "@erp/core/components";
import { useReturnToNavigation } from "@erp/core/hooks"; import { useReturnToNavigation } from "@erp/core/hooks";
import { RdxPageContainer } from "@repo/rdx-ui/components"; import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { import {
Button, Button,
ResizableHandle, ResizableHandle,
@ -14,6 +14,7 @@ import { createSearchParams, useNavigate } from "react-router-dom";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
import { useListCustomersPageController } from "../../controllers"; import { useListCustomersPageController } from "../../controllers";
import { CustomerSummaryPanel, CustomersGrid, useCustomersGridColumns } from "../blocks"; import { CustomerSummaryPanel, CustomersGrid, useCustomersGridColumns } from "../blocks";
import { ListCustomersPageView } from "./list-customers-page-view"; import { ListCustomersPageView } from "./list-customers-page-view";
export const ListCustomersPage = () => { export const ListCustomersPage = () => {
@ -90,8 +91,8 @@ export const ListCustomersPage = () => {
if (pageError) { if (pageError) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={pageError.description} description={pageError.description}
primaryAction={ primaryAction={
@ -110,8 +111,8 @@ export const ListCustomersPage = () => {
title={pageError.title} title={pageError.title}
variant={pageError.variant} variant={pageError.variant}
/> />
</div>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
} }

View File

@ -1,9 +0,0 @@
import type { PropsWithChildren } from "react";
export const CustomerLayout = ({ children }: PropsWithChildren) => {
return (
<div className="flex flex-col h-full w-full" id="customers">
{children}
</div>
);
};

View File

@ -1 +0,0 @@
export * from "./customer-layout";

View File

@ -1,9 +1,10 @@
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components"; import { PageKeyboardShortcutsButton } from "@erp/core/components";
import { import {
CancelActionButton, CancelActionButton,
FormActionsBar, FormActionsBar,
type FormSecondaryAction, type FormSecondaryAction,
FormSecondaryActionsMenu, FormSecondaryActionsMenu,
RdxEditorHeader,
RhfSubmitActionButton, RhfSubmitActionButton,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@ -75,7 +76,7 @@ export const CustomerUpdateHeader = ({
]; ];
return ( return (
<PageFormHeader <RdxEditorHeader
actions={ actions={
<FormActionsBar align="end" reverseOnMobile={false}> <FormActionsBar align="end" reverseOnMobile={false}>
<PageKeyboardShortcutsButton <PageKeyboardShortcutsButton
@ -114,10 +115,10 @@ export const CustomerUpdateHeader = ({
className={className} className={className}
onBack={onCancel} onBack={onCancel}
showStatus={hasChanges} showStatus={hasChanges}
statusLabel={labels.modified} status={labels.modified}
title={labels.title} title={labels.title}
> >
{children} {children}
</PageFormHeader> </RdxEditorHeader>
); );
}; };

View File

@ -1,4 +1,4 @@
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { BackHistoryButton, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
@ -6,8 +6,8 @@ import { useTranslation } from "../../../i18n";
export const CustomerEditorSkeleton = () => { export const CustomerEditorSkeleton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <RdxPageFrame>
<AppContent> <RdxPageContainer width="content">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div aria-hidden="true" className="space-y-2"> <div aria-hidden="true" className="space-y-2">
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" /> <div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
@ -26,7 +26,7 @@ export const CustomerEditorSkeleton = () => {
<div className="h-28 w-full rounded-md bg-muted animate-pulse" /> <div className="h-28 w-full rounded-md bg-muted animate-pulse" />
</div> </div>
<span className="sr-only">{t("update.loading")}</span> <span className="sr-only">{t("update.loading")}</span>
</AppContent> </RdxPageContainer>
</> </RdxPageFrame>
); );
}; };

View File

@ -1,6 +1,6 @@
import { ErrorAlert, PageErrorState } from "@erp/core/components"; import { ErrorAlert, PageErrorState } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import { RdxEditorPageLayout } from "@repo/rdx-ui/components"; import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { FormProvider } from "react-hook-form"; import { FormProvider } from "react-hook-form";
import { useTranslation } from "../../../i18n"; import { useTranslation } from "../../../i18n";
@ -26,8 +26,8 @@ export const CustomerUpdatePage = () => {
if (ctrl.pageError) { if (ctrl.pageError) {
return ( return (
<div className="p-4 sm:p-6"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={ctrl.pageError.description} description={ctrl.pageError.description}
primaryAction={ primaryAction={
@ -61,15 +61,17 @@ export const CustomerUpdatePage = () => {
title={ctrl.pageError.title} title={ctrl.pageError.title}
variant={ctrl.pageError.variant} variant={ctrl.pageError.variant}
/> />
</div> </RdxPageContainer>
</div> </RdxPageFrame>
); );
} }
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden">
<FormProvider {...updateCtrl.form}> <FormProvider {...updateCtrl.form}>
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}> <UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
<RdxPageFrame
contentClassName="bg-muted/20"
header={
<CustomerUpdateHeader <CustomerUpdateHeader
formId={updateCtrl.formId} formId={updateCtrl.formId}
hasChanges={updateCtrl.form.formState.isDirty} hasChanges={updateCtrl.form.formState.isDirty}
@ -91,9 +93,10 @@ export const CustomerUpdatePage = () => {
//onDuplicate={handleDuplicate} //onDuplicate={handleDuplicate}
//onExportPdf={handleExportPdf} //onExportPdf={handleExportPdf}
/> />
}
<div className="flex-1 overflow-y-auto bg-muted/20"> >
<RdxEditorPageLayout width="simple"> <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="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="flex min-w-0 flex-col gap-6">
{updateCtrl.isUpdateError ? ( {updateCtrl.isUpdateError ? (
@ -121,10 +124,10 @@ export const CustomerUpdatePage = () => {
/> />
</div> </div>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</div> </RdxPageContainer>
</RdxPageFrame>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</FormProvider> </FormProvider>
</div>
); );
}; };

View File

@ -1,9 +1,10 @@
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components"; import { PageKeyboardShortcutsButton } from "@erp/core/components";
import { import {
CancelActionButton, CancelActionButton,
FormActionsBar, FormActionsBar,
type FormSecondaryAction, type FormSecondaryAction,
FormSecondaryActionsMenu, FormSecondaryActionsMenu,
RdxEditorHeader,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@ -70,7 +71,7 @@ export const CustomerViewHeader = ({
]; ];
return ( return (
<PageFormHeader <RdxEditorHeader
actions={ actions={
<FormActionsBar align="end" reverseOnMobile={false}> <FormActionsBar align="end" reverseOnMobile={false}>
<PageKeyboardShortcutsButton <PageKeyboardShortcutsButton
@ -101,10 +102,10 @@ export const CustomerViewHeader = ({
className={className} className={className}
onBack={onCancel} onBack={onCancel}
showStatus={hasChanges} showStatus={hasChanges}
statusLabel={labels.modified} status={labels.modified}
title={labels.title} title={labels.title}
> >
{children} {children}
</PageFormHeader> </RdxEditorHeader>
); );
}; };

View File

@ -1,5 +1,6 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { useReturnToNavigation } from "@erp/core/hooks"; import { useReturnToNavigation } from "@erp/core/hooks";
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components"; import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
import { import {
Banknote, Banknote,
@ -35,8 +36,8 @@ export const CustomerViewPage = () => {
if (ctrl.pageError) { if (ctrl.pageError) {
return ( return (
<div className="p-4 sm:p-6"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={ctrl.pageError.description} description={ctrl.pageError.description}
primaryAction={ primaryAction={
@ -70,13 +71,14 @@ export const CustomerViewPage = () => {
title={ctrl.pageError.title} title={ctrl.pageError.title}
variant={ctrl.pageError.variant} variant={ctrl.pageError.variant}
/> />
</div> </RdxPageContainer>
</div> </RdxPageFrame>
); );
} }
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden"> <RdxPageFrame
header={
<CustomerViewHeader <CustomerViewHeader
labels={{ labels={{
title: "Ver cliente", title: "Ver cliente",
@ -94,8 +96,9 @@ export const CustomerViewPage = () => {
//onDuplicate={handleDuplicate} //onDuplicate={handleDuplicate}
//onExportPdf={handleExportPdf} //onExportPdf={handleExportPdf}
/> />
}
<div className="flex-1 overflow-y-auto"> >
<RdxPageContainer className="space-y-6" width="content">
{/* Información Básica */} {/* Información Básica */}
<Card> <Card>
<CardHeader> <CardHeader>
@ -369,7 +372,7 @@ export const CustomerViewPage = () => {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </RdxPageContainer>
</div> </RdxPageFrame>
); );
}; };

View File

@ -88,7 +88,7 @@ Contenido visible actual:
- información personal: `name`, `email`, `language_code` - información personal: `name`, `email`, `language_code`
- modo `app-fullscreen`, sin sidebar principal - 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 - seguridad mediante dialog para cambio de contraseña
- cierre de sesión reutilizando el flujo de logout existente - 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,9 +1,12 @@
import { ErrorAlert, PageErrorState, PageFormHeader } from "@erp/core/components"; import { ErrorAlert } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import { import {
CancelActionButton, CancelActionButton,
RdxEditorActionBar, RdxEditorActionBar,
RdxEditorPageLayout, RdxEditorHeader,
RdxEditorLayout,
RdxPageContainer,
RdxPageFrame,
RdxPanel, RdxPanel,
RhfSubmitActionButton, RhfSubmitActionButton,
SelectField, SelectField,
@ -26,65 +29,7 @@ import { useTranslation } from "../../../i18n";
import { useAccountPageController } from "../../controllers"; import { useAccountPageController } from "../../controllers";
import type { AccountPasswordForm, AccountProfileForm } from "../../entities"; import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
const AccountLoadingState = ({ label }: { label: string }) => { import { AccountErrorState, AccountLoadingState } from "./account-page-states";
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>
);
};
const AccountPasswordDialog = ({ const AccountPasswordDialog = ({
ctrl, ctrl,
@ -251,15 +196,18 @@ export const AccountPage = () => {
}; };
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden"> <RdxPageFrame
<PageFormHeader contentClassName="bg-muted/20"
header={
<RdxEditorHeader
backLabel={t("account.header.back")} backLabel={t("account.header.back")}
onBack={navigateBack} onBack={navigateBack}
title={t("account.title")} title={t("account.title")}
/> />
}
<div className="flex-1 overflow-y-auto bg-muted/20"> >
<RdxEditorPageLayout width="simple"> <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="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="flex min-w-0 flex-col gap-6">
<FormProvider {...ctrl.profileForm}> <FormProvider {...ctrl.profileForm}>
@ -404,8 +352,8 @@ export const AccountPage = () => {
</RdxPanel> </RdxPanel>
</div> </div>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</div> </RdxPageContainer>
<AccountPasswordDialog <AccountPasswordDialog
ctrl={ctrl} ctrl={ctrl}
@ -414,6 +362,6 @@ export const AccountPage = () => {
onOpenChange={handlePasswordDialogOpenChange} onOpenChange={handlePasswordDialogOpenChange}
t={t} t={t}
/> />
</div> </RdxPageFrame>
); );
}; };

View File

@ -1,6 +1,6 @@
import { ErrorAlert } from "@erp/core/components"; import { ErrorAlert } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import { RdxEditorPageLayout } from "@repo/rdx-ui/components"; import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { FormProvider } from "react-hook-form"; import { FormProvider } from "react-hook-form";
import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom"; import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom";
@ -24,9 +24,11 @@ export const SupplierCreatePage = () => {
}); });
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden">
<FormProvider {...controller.form}> <FormProvider {...controller.form}>
<UnsavedChangesProvider isDirty={controller.form.formState.isDirty}> <UnsavedChangesProvider isDirty={controller.form.formState.isDirty}>
<RdxPageFrame
contentClassName="bg-muted/20"
header={
<SupplierEditorHeader <SupplierEditorHeader
formId={controller.formId} formId={controller.formId}
hasChanges={controller.form.formState.isDirty} hasChanges={controller.form.formState.isDirty}
@ -34,8 +36,10 @@ export const SupplierCreatePage = () => {
onCancel={navigateBack} onCancel={navigateBack}
title={t("create.title")} title={t("create.title")}
/> />
<div className="flex-1 overflow-y-auto bg-muted/20"> }
<RdxEditorPageLayout width="simple"> >
<RdxPageContainer width="content">
<RdxEditorLayout>
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6"> <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"> <div className="flex min-w-0 flex-col gap-6">
{controller.mutation.isError ? ( {controller.mutation.isError ? (
@ -51,10 +55,10 @@ export const SupplierCreatePage = () => {
/> />
</div> </div>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</div> </RdxPageContainer>
</RdxPageFrame>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</FormProvider> </FormProvider>
</div>
); );
}; };

View File

@ -2,6 +2,7 @@ import { PageErrorState, SimpleSearchInput } from "@erp/core/components";
import { import {
RdxPageActions, RdxPageActions,
RdxPageContainer, RdxPageContainer,
RdxPageFrame,
RdxPageHeader, RdxPageHeader,
RdxPageStack, RdxPageStack,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
@ -36,7 +37,8 @@ export const SuppliersListPage = () => {
if (controller.isError && controller.data.items.length === 0) { if (controller.isError && controller.data.items.length === 0) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={t("list.error.description")} description={t("list.error.description")}
primaryAction={{ primaryAction={{
@ -49,10 +51,12 @@ export const SuppliersListPage = () => {
variant="unknown" variant="unknown"
/> />
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
} }
return ( return (
<RdxPageFrame>
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default"> <RdxPageStack spacing="default">
<RdxPageHeader <RdxPageHeader
@ -94,5 +98,6 @@ export const SuppliersListPage = () => {
/> />
</RdxPageStack> </RdxPageStack>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

@ -1,5 +1,4 @@
export * from "./supplier-editor-form"; export * from "./supplier-editor-form";
export * from "./supplier-editor-header"; export * from "./supplier-editor-header";
export * from "./supplier-editor-skeleton"; export * from "./supplier-editor-skeleton";
export * from "./supplier-layout";
export * from "./supplier-status-badge"; export * from "./supplier-status-badge";

View File

@ -1,5 +1,10 @@
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components"; import { PageKeyboardShortcutsButton } from "@erp/core/components";
import { CancelActionButton, FormActionsBar, RhfSubmitActionButton } from "@repo/rdx-ui/components"; import {
CancelActionButton,
FormActionsBar,
RdxEditorHeader,
RhfSubmitActionButton,
} from "@repo/rdx-ui/components";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
@ -19,7 +24,7 @@ export const SupplierEditorHeader = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<PageFormHeader <RdxEditorHeader
actions={ actions={
<FormActionsBar align="end" reverseOnMobile={false}> <FormActionsBar align="end" reverseOnMobile={false}>
<PageKeyboardShortcutsButton <PageKeyboardShortcutsButton
@ -49,7 +54,7 @@ export const SupplierEditorHeader = ({
backLabel={t("common.actions.back")} backLabel={t("common.actions.back")}
onBack={onCancel} onBack={onCancel}
showStatus={hasChanges} showStatus={hasChanges}
statusLabel={t("form.modified")} status={t("form.modified")}
title={title} title={title}
/> />
); );

View File

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

View File

@ -1,7 +0,0 @@
import type { PropsWithChildren } from "react";
export const SupplierLayout = ({ children }: PropsWithChildren) => (
<div className="flex h-full w-full flex-col" id="suppliers">
{children}
</div>
);

View File

@ -2,9 +2,6 @@ import type { ModuleClientParams } from "@erp/core/client";
import { lazy } from "react"; import { lazy } from "react";
import { Outlet, type RouteObject } from "react-router-dom"; import { Outlet, type RouteObject } from "react-router-dom";
const SupplierLayout = lazy(() =>
import("./shared/ui").then((module) => ({ default: module.SupplierLayout }))
);
const SuppliersListPage = lazy(() => const SuppliersListPage = lazy(() =>
import("./list").then((module) => ({ default: module.SuppliersListPage })) import("./list").then((module) => ({ default: module.SuppliersListPage }))
); );
@ -22,38 +19,22 @@ export const SupplierRoutes = (params: ModuleClientParams): RouteObject[] => [
{ {
path: "suppliers", path: "suppliers",
handle: { layout: "app-sidebar", protected: true }, handle: { layout: "app-sidebar", protected: true },
element: ( element: <Outlet context={params} />,
<SupplierLayout>
<Outlet context={params} />
</SupplierLayout>
),
children: [{ index: true, element: <SuppliersListPage /> }], children: [{ index: true, element: <SuppliersListPage /> }],
}, },
{ {
path: "suppliers/create", path: "suppliers/create",
handle: { layout: "app-fullscreen", protected: true }, handle: { layout: "app-fullscreen", protected: true },
element: ( element: <SupplierCreatePage />,
<SupplierLayout>
<SupplierCreatePage />
</SupplierLayout>
),
}, },
{ {
path: "suppliers/:id/edit", path: "suppliers/:id/edit",
handle: { layout: "app-fullscreen", protected: true }, handle: { layout: "app-fullscreen", protected: true },
element: ( element: <SupplierUpdatePage />,
<SupplierLayout>
<SupplierUpdatePage />
</SupplierLayout>
),
}, },
{ {
path: "suppliers/:id", path: "suppliers/:id",
handle: { layout: "app-fullscreen", protected: true }, handle: { layout: "app-fullscreen", protected: true },
element: ( element: <SupplierDetailPage />,
<SupplierLayout>
<SupplierDetailPage />
</SupplierLayout>
),
}, },
]; ];

View File

@ -1,6 +1,6 @@
import { ErrorAlert, PageErrorState } from "@erp/core/components"; import { ErrorAlert, PageErrorState } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import { RdxEditorPageLayout, RdxPageContainer } from "@repo/rdx-ui/components"; import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { FormProvider } from "react-hook-form"; import { FormProvider } from "react-hook-form";
import { createSearchParams, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { createSearchParams, useNavigate, useParams, useSearchParams } from "react-router-dom";
@ -28,7 +28,8 @@ export const SupplierUpdatePage = () => {
if (controller.query.isError || !controller.query.data) { if (controller.query.isError || !controller.query.data) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={t("detail.error.description")} description={t("detail.error.description")}
primaryAction={{ primaryAction={{
@ -42,13 +43,16 @@ export const SupplierUpdatePage = () => {
variant="notFound" variant="notFound"
/> />
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
} }
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden">
<FormProvider {...controller.form}> <FormProvider {...controller.form}>
<UnsavedChangesProvider isDirty={controller.form.formState.isDirty}> <UnsavedChangesProvider isDirty={controller.form.formState.isDirty}>
<RdxPageFrame
contentClassName="bg-muted/20"
header={
<SupplierEditorHeader <SupplierEditorHeader
formId={controller.formId} formId={controller.formId}
hasChanges={controller.form.formState.isDirty} hasChanges={controller.form.formState.isDirty}
@ -56,8 +60,10 @@ export const SupplierUpdatePage = () => {
onCancel={navigateBack} onCancel={navigateBack}
title={t("update.title")} title={t("update.title")}
/> />
<div className="flex-1 overflow-y-auto bg-muted/20"> }
<RdxEditorPageLayout width="simple"> >
<RdxPageContainer width="content">
<RdxEditorLayout>
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6"> <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"> <div className="flex min-w-0 flex-col gap-6">
{controller.mutation.isError ? ( {controller.mutation.isError ? (
@ -73,10 +79,10 @@ export const SupplierUpdatePage = () => {
/> />
</div> </div>
</div> </div>
</RdxEditorPageLayout> </RdxEditorLayout>
</div> </RdxPageContainer>
</RdxPageFrame>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</FormProvider> </FormProvider>
</div>
); );
}; };

View File

@ -3,6 +3,7 @@ import { useReturnToNavigation } from "@erp/core/hooks";
import { import {
RdxPageActions, RdxPageActions,
RdxPageContainer, RdxPageContainer,
RdxPageFrame,
RdxPageHeader, RdxPageHeader,
RdxPageStack, RdxPageStack,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
@ -35,7 +36,8 @@ export const SupplierDetailPage = () => {
if (query.isError || !query.data) { if (query.isError || !query.data) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageContainer width="content">
<PageErrorState <PageErrorState
description={t("detail.error.description")} description={t("detail.error.description")}
primaryAction={{ primaryAction={{
@ -49,6 +51,7 @@ export const SupplierDetailPage = () => {
variant="notFound" variant="notFound"
/> />
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
} }
@ -56,8 +59,9 @@ export const SupplierDetailPage = () => {
const context = supplier.reference || supplier.tin || supplier.tradeName || "-"; const context = supplier.reference || supplier.tin || supplier.tradeName || "-";
return ( return (
<RdxPageFrame
header={
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageStack spacing="default">
<RdxPageHeader <RdxPageHeader
actions={ actions={
<RdxPageActions <RdxPageActions
@ -89,7 +93,11 @@ export const SupplierDetailPage = () => {
</span> </span>
} }
/> />
</RdxPageContainer>
}
>
<RdxPageContainer width="full">
<RdxPageStack spacing="default">
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
<Card> <Card>
<CardHeader> <CardHeader>
@ -122,9 +130,15 @@ export const SupplierDetailPage = () => {
<CardContent> <CardContent>
<dl className="grid gap-4 sm:grid-cols-2"> <dl className="grid gap-4 sm:grid-cols-2">
<DetailField label={t("fields.primary_email")} value={supplier.primaryEmail} /> <DetailField label={t("fields.primary_email")} value={supplier.primaryEmail} />
<DetailField label={t("fields.secondary_email")} value={supplier.secondaryEmail} /> <DetailField
label={t("fields.secondary_email")}
value={supplier.secondaryEmail}
/>
<DetailField label={t("fields.primary_phone")} value={supplier.primaryPhone} /> <DetailField label={t("fields.primary_phone")} value={supplier.primaryPhone} />
<DetailField label={t("fields.secondary_phone")} value={supplier.secondaryPhone} /> <DetailField
label={t("fields.secondary_phone")}
value={supplier.secondaryPhone}
/>
<DetailField label={t("fields.primary_mobile")} value={supplier.primaryMobile} /> <DetailField label={t("fields.primary_mobile")} value={supplier.primaryMobile} />
<DetailField <DetailField
label={t("fields.secondary_mobile")} label={t("fields.secondary_mobile")}
@ -172,5 +186,6 @@ export const SupplierDetailPage = () => {
</div> </div>
</RdxPageStack> </RdxPageStack>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame>
); );
}; };

View File

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

View File

@ -10,7 +10,7 @@ export interface RdxEditorActionBarProps extends Omit<ComponentPropsWithoutRef<"
} }
/** /**
* Barra inferior de acciones de una pantalla de edición. * Barra inferior de acciones de un editor.
* *
* Puede mostrar: * Puede mostrar:
* - contenido auxiliar a la izquierda * - contenido auxiliar a la izquierda

View File

@ -6,47 +6,49 @@ import { renderToStaticMarkup } from "react-dom/server";
import { RdxFormGrid } from "../form/rdx-form-grid.tsx"; import { RdxFormGrid } from "../form/rdx-form-grid.tsx";
import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx"; import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx"; import { RdxEditorHeader } from "./rdx-editor-header.tsx";
import { RdxEditorLayout } from "./rdx-editor-layout.tsx";
test("RdxEditorLayout only distributes main content and an optional aside", () => {
const html = renderToStaticMarkup(
<RdxEditorLayout aside={<span>Aside</span>}>Main</RdxEditorLayout>
);
assert.match(html, /grid/);
assert.match(html, /<aside/);
assert.doesNotMatch(html, /overflow/);
assert.doesNotMatch(html, /px-4/);
});
test("RdxEditorHeader renderiza navegación, título, estado, acciones y contenido contextual", () => {
const html = renderToStaticMarkup(
<RdxEditorHeader
actions={<span>Actions</span>}
backLabel="Volver"
onBack={() => undefined}
status="Modificada"
title="Editar proforma"
>
<div>Secondary content</div>
</RdxEditorHeader>
);
assert.match(html, /aria-label="Volver"/);
assert.match(html, /<h1[^>]*>Editar proforma<\/h1>/);
assert.match(html, /Modificada/);
assert.match(html, /Actions/);
assert.match(html, /Secondary content/);
assert.doesNotMatch(html, /sticky top-0/);
});
test("RdxEditorHeader no renderiza back sin callback", () => {
const html = renderToStaticMarkup(<RdxEditorHeader backLabel="Volver" title="Detalle" />);
assert.doesNotMatch(html, /aria-label="Volver"/);
});
import { RdxPanel } from "./rdx-panel.tsx"; 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", () => { test("RdxPanel plain renderiza header semántico y contenido sin card", () => {
const html = renderToStaticMarkup( const html = renderToStaticMarkup(
<RdxPanel <RdxPanel

View File

@ -1,53 +1,53 @@
import { RdxBadge } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { ArrowLeftIcon } from "lucide-react"; import { ArrowLeftIcon } from "lucide-react";
import type { ReactNode } from "react"; import type { ComponentPropsWithoutRef, ReactNode } from "react";
export interface PageFormHeaderProps { import { RdxBadge } from "../rdx-badge.tsx";
title: string | ReactNode;
backLabel: string;
onBack?: () => void;
statusLabel?: string;
showStatus?: boolean;
export interface RdxEditorHeaderProps
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
title: ReactNode;
actions?: ReactNode; actions?: ReactNode;
backLabel?: string;
onBack?: () => void;
status?: ReactNode;
showStatus?: boolean;
children?: ReactNode; children?: ReactNode;
className?: string;
contentClassName?: string; contentClassName?: string;
} }
export const PageFormHeader = ({ /**
* Cabecera compacta para páginas fullscreen.
* Presenta navegación de retorno, título, estado y acciones.
*/
export const RdxEditorHeader = ({
title, title,
actions,
backLabel, backLabel,
onBack, onBack,
statusLabel, status,
showStatus = false, showStatus = true,
actions,
children, children,
className,
contentClassName, contentClassName,
}: PageFormHeaderProps) => { className,
...props
}: RdxEditorHeaderProps) => {
return ( return (
<header <header
className={cn( className={cn("z-50 shrink-0 bg-background ring-1 ring-foreground/10 shadow-xs", className)}
"sticky top-0 z-50 ring-1 ring-foreground/10 bg-background shadow-xs", {...props}
className
)}
> >
<div <div
className={cn( className={cn(
"flex h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:h-18", "flex min-h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:min-h-18",
contentClassName contentClassName
)} )}
> >
<div className="flex min-w-0 items-center gap-3"> <div className="flex min-w-0 items-center gap-3">
{onBack ? (
<Button <Button
aria-label={backLabel} aria-label={backLabel}
disabled={!onBack}
onClick={onBack} onClick={onBack}
size="icon" size="icon"
type="button" type="button"
@ -55,20 +55,21 @@ export const PageFormHeader = ({
> >
<ArrowLeftIcon aria-hidden="true" className="size-5" /> <ArrowLeftIcon aria-hidden="true" className="size-5" />
</Button> </Button>
) : null}
<div className="flex min-w-0 items-center gap-4"> <div className="flex min-w-0 items-center gap-4">
<h1 className="min-w-0 truncate text-xl font-semibold tracking-tight text-foreground lg:text-2xl"> <h1 className="min-w-0 truncate text-xl font-semibold tracking-tight text-foreground lg:text-2xl">
{title} {title}
</h1> </h1>
{statusLabel ? ( {status ? (
<RdxBadge <RdxBadge
className={cn( className={cn(
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200", "border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
showStatus ? "opacity-100" : "pointer-events-none opacity-0" showStatus ? "opacity-100" : "pointer-events-none opacity-0"
)} )}
> >
{statusLabel} {status}
</RdxBadge> </RdxBadge>
) : null} ) : null}
</div> </div>

View File

@ -0,0 +1,31 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
export interface RdxEditorLayoutProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
children: ReactNode;
aside?: ReactNode;
}
/**
* Distribuye el contenido interno de un editor entre una región principal
* y un aside opcional.
*/
export const RdxEditorLayout = ({ children, aside, className, ...props }: RdxEditorLayoutProps) => {
if (!aside) {
return (
<div className={cn("min-w-0", className)} {...props}>
{children}
</div>
);
}
return (
<div
className={cn("grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1fr)_22rem]", className)}
{...props}
>
<div className="min-w-0">{children}</div>
<aside className="min-w-0">{aside}</aside>
</div>
);
};

View File

@ -1,56 +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]",
};
/**
* 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.
*/
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>
);
};

View File

@ -43,7 +43,6 @@ export const RdxPanel = ({
...props ...props
}: RdxPanelProps) => { }: RdxPanelProps) => {
const titleId = useId(); const titleId = useId();
const hasHeader = Boolean(title || description || icon || actions);
return ( return (
<section <section
@ -51,7 +50,6 @@ export const RdxPanel = ({
className={cn(VARIANT_CLASSNAME[variant], className)} className={cn(VARIANT_CLASSNAME[variant], className)}
{...props} {...props}
> >
{hasHeader ? (
<div className={cn("flex flex-col gap-4", variant === "plain" ? "pb-4" : "pb-5")}> <div className={cn("flex flex-col gap-4", variant === "plain" ? "pb-4" : "pb-5")}>
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-start gap-3"> <div className="flex min-w-0 items-start gap-3">
@ -69,16 +67,13 @@ export const RdxPanel = ({
{title} {title}
</h2> </h2>
{description ? ( {description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
<p className="text-sm text-muted-foreground">{description}</p>
) : null}
</div> </div>
</div> </div>
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null} {actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
</div> </div>
</div> </div>
) : null}
<div className="min-w-0">{children}</div> <div className="min-w-0">{children}</div>
</section> </section>

View File

@ -3,5 +3,6 @@ export * from "./app-header.tsx";
export * from "./app-layout.tsx"; export * from "./app-layout.tsx";
export * from "./rdx-page-actions.tsx"; export * from "./rdx-page-actions.tsx";
export * from "./rdx-page-container.tsx"; export * from "./rdx-page-container.tsx";
export * from "./rdx-page-frame.tsx";
export * from "./rdx-page-header.tsx"; export * from "./rdx-page-header.tsx";
export * from "./rdx-page-stack.tsx"; export * from "./rdx-page-stack.tsx";

View File

@ -1,12 +1,27 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { RdxPageActions } from "./rdx-page-actions.tsx"; import { RdxPageActions } from "./rdx-page-actions.tsx";
import { RdxPageContainer } from "./rdx-page-container.tsx"; import { RdxPageContainer } from "./rdx-page-container.tsx";
import { RdxPageFrame } from "./rdx-page-frame.tsx";
import { RdxPageHeader } from "./rdx-page-header.tsx"; import { RdxPageHeader } from "./rdx-page-header.tsx";
import { RdxPageStack } from "./rdx-page-stack.tsx"; import { RdxPageStack } from "./rdx-page-stack.tsx";
test("RdxPageFrame separates fixed regions from the scrollable content", () => {
const html = renderToStaticMarkup(
<RdxPageFrame footer={<span>Footer</span>} header={<span>Header</span>}>
Content
</RdxPageFrame>
);
assert.match(html, /overflow-hidden/);
assert.match(html, /overflow-y-auto/);
assert.ok(html.indexOf("Header") < html.indexOf("Content"));
assert.ok(html.indexOf("Content") < html.indexOf("Footer"));
});
test("RdxPageContainer renders children with full width by default", () => { test("RdxPageContainer renders children with full width by default", () => {
const html = renderToStaticMarkup(<RdxPageContainer>Content</RdxPageContainer>); const html = renderToStaticMarkup(<RdxPageContainer>Content</RdxPageContainer>);
@ -91,29 +106,6 @@ test("RdxPageHeader omits optional wrappers when content is absent", () => {
assert.doesNotMatch(html, /Metadata/); assert.doesNotMatch(html, /Metadata/);
}); });
test("RdxPageHeader soporta el modo sticky de editor con navegación, estado y contenido secundario", () => {
const html = renderToStaticMarkup(
<RdxPageHeader
actions={<span>Actions</span>}
backLabel="Volver"
onBack={() => undefined}
showStatus
statusLabel="Modificada"
sticky
title="Editar proforma"
>
<div>Secondary content</div>
</RdxPageHeader>
);
assert.match(html, /sticky top-0/);
assert.match(html, /aria-label="Volver"/);
assert.match(html, /Editar proforma/);
assert.match(html, /Modificada/);
assert.match(html, /Actions/);
assert.match(html, /Secondary content/);
});
test("RdxPageActions renders only populated regions in the expected order", () => { test("RdxPageActions renders only populated regions in the expected order", () => {
const html = renderToStaticMarkup( const html = renderToStaticMarkup(
<RdxPageActions <RdxPageActions

View File

@ -0,0 +1,35 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
export interface RdxPageFrameProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
children: ReactNode;
header?: ReactNode;
footer?: ReactNode;
contentClassName?: string;
}
/**
* Estructura una página en cabecera, contenido scrollable y pie opcional.
* Centraliza el control del scroll principal de la página.
*/
export const RdxPageFrame = ({
children,
header,
footer,
contentClassName,
className,
...props
}: RdxPageFrameProps) => {
return (
<div
className={cn("flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", className)}
{...props}
>
{header ? <div className="shrink-0">{header}</div> : null}
<div className={cn("min-h-0 min-w-0 flex-1 overflow-y-auto", contentClassName)}>
{children}
</div>
{footer ? <div className="shrink-0">{footer}</div> : null}
</div>
);
};

View File

@ -1,10 +1,6 @@
import { Button } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { ArrowLeftIcon } from "lucide-react";
import type { ComponentPropsWithoutRef, ReactNode } from "react"; import type { ComponentPropsWithoutRef, ReactNode } from "react";
import { RdxBadge } from "../rdx-badge.tsx";
export interface RdxPageHeaderProps export interface RdxPageHeaderProps
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> { extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
title: ReactNode; title: ReactNode;
@ -12,27 +8,13 @@ export interface RdxPageHeaderProps
leading?: ReactNode; leading?: ReactNode;
metadata?: ReactNode; metadata?: ReactNode;
actions?: ReactNode; actions?: ReactNode;
backLabel?: string;
onBack?: () => void;
statusLabel?: ReactNode;
showStatus?: boolean;
sticky?: boolean;
contentClassName?: string;
children?: ReactNode; children?: ReactNode;
} }
/** /**
* Cabecera estándar de una página. * Cabecera estándar de una página.
* *
* Muestra: * Presenta título, descripción, metadata y acciones.
* - navegación de retorno opcional
* - título
* - descripción o metadata opcional
* - estado opcional
* - acciones de página
*
* Puede permanecer fija dentro de un contenedor scrollable
* mediante `sticky`.
*/ */
export const RdxPageHeader = ({ export const RdxPageHeader = ({
@ -41,73 +23,10 @@ export const RdxPageHeader = ({
leading, leading,
metadata, metadata,
actions, actions,
backLabel,
onBack,
statusLabel,
showStatus = false,
sticky = false,
contentClassName,
children, children,
className, className,
...props ...props
}: RdxPageHeaderProps) => { }: RdxPageHeaderProps) => {
if (sticky) {
const shouldRenderBackButton = Boolean(backLabel || onBack);
return (
<header
className={cn(
"sticky top-0 z-50 shrink-0 bg-background ring-1 ring-foreground/10 shadow-xs",
className
)}
{...props}
>
<div
className={cn(
"flex min-h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:min-h-18",
contentClassName
)}
>
<div className="flex min-w-0 items-center gap-3">
{shouldRenderBackButton ? (
<Button
aria-label={backLabel}
disabled={!onBack}
onClick={onBack}
size="icon"
type="button"
variant="ghost"
>
<ArrowLeftIcon aria-hidden="true" className="size-5" />
</Button>
) : null}
<div className="flex min-w-0 items-center gap-4">
<h1 className="min-w-0 truncate text-xl font-semibold tracking-tight text-foreground lg:text-2xl">
{title}
</h1>
{statusLabel ? (
<RdxBadge
className={cn(
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
)}
>
{statusLabel}
</RdxBadge>
) : null}
</div>
</div>
{actions ? <div className="flex items-center gap-4">{actions}</div> : null}
</div>
{children}
</header>
);
}
return ( return (
<header <header
className={cn( className={cn(

View File

@ -16,8 +16,8 @@ const SPACING_CLASSNAME: Record<RdxPageStackSpacing, string> = {
}; };
/** /**
* Contenedor para layout para apilar contenido verticalmente. * Apila contenido verticalmente aplicando un espaciado consistente.
* No proporciona estructura de página ni controla scroll. * No controla viewport, scroll ni estructura de página.
*/ */
export const RdxPageStack = ({ export const RdxPageStack = ({