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.
This commit is contained in:
David Arranz 2026-09-04 13:30:22 +02:00
parent 85d2f11bde
commit 2f8122ca77
35 changed files with 691 additions and 738 deletions

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,63 +56,65 @@ export const HomePage = () => {
title={ctrl.pageError.title} title={ctrl.pageError.title}
variant="server" variant="server"
/> />
</div> </RdxPageContainer>
</RdxPageContainer> </RdxPageFrame>
); );
} }
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageStack spacing="default"> <RdxPageContainer width="full">
<RdxPageHeader <RdxPageStack spacing="default">
description={t("web.home.header.description")} <RdxPageHeader
title={t("web.home.header.title")} description={t("web.home.header.description")}
/> title={t("web.home.header.title")}
/>
{ctrl.customersTotalQuery.isLoading || {ctrl.customersTotalQuery.isLoading ||
ctrl.proformasOverviewQuery.isLoading || ctrl.proformasOverviewQuery.isLoading ||
ctrl.monthlyBillingQuery.isLoading ? ( ctrl.monthlyBillingQuery.isLoading ? (
<HomeKpisSkeleton /> <HomeKpisSkeleton />
) : ( ) : (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{ctrl.kpis.map((kpi) => ( {ctrl.kpis.map((kpi) => (
<HomeKpiCard item={kpi} key={kpi.id} /> <HomeKpiCard item={kpi} key={kpi.id} />
))} ))}
</div>
)}
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
<HomeAttentionPanel
hasError={ctrl.proformasOverviewQuery.isError}
items={ctrl.attentionItems}
/>
<HomeProformaStatusSummary
hasError={ctrl.proformasOverviewQuery.isError}
items={ctrl.proformaStatusSummary}
/>
</div> </div>
)}
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
<HomeAttentionPanel <HomeRecentActivity
hasError={ctrl.proformasOverviewQuery.isError} hasPartialError={ctrl.recentActivityQuery.hasPartialError}
items={ctrl.attentionItems} items={ctrl.recentActivityQuery.items}
/> />
<HomeProformaStatusSummary <HomeMonthlyBillingSummary
hasError={ctrl.proformasOverviewQuery.isError} data={ctrl.monthlyBillingQuery.data}
items={ctrl.proformaStatusSummary} hasError={ctrl.monthlyBillingQuery.isError}
/> />
</div> </div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]"> <p className="text-xs text-muted-foreground">
<HomeRecentActivity {t("web.home.footer.note")}{" "}
hasPartialError={ctrl.recentActivityQuery.hasPartialError} <Link
items={ctrl.recentActivityQuery.items} className="underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/> to="/customer-invoices"
<HomeMonthlyBillingSummary >
data={ctrl.monthlyBillingQuery.data} {t("web.home.footer.link")}
hasError={ctrl.monthlyBillingQuery.isError} </Link>
/> </p>
</div> </RdxPageStack>
</RdxPageContainer>
<p className="text-xs text-muted-foreground"> </RdxPageFrame>
{t("web.home.footer.note")}{" "}
<Link
className="underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
to="/customer-invoices"
>
{t("web.home.footer.link")}
</Link>
</p>
</RdxPageStack>
</RdxPageContainer>
); );
}; };

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,27 +16,32 @@ 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 (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageStack spacing="default"> <RdxPageContainer width="full">
<RdxPageHeader <RdxPageStack spacing="default">
description={t("web.settings.description")} <RdxPageHeader
title={t("web.settings.title")} description={t("web.settings.description")}
/> title={t("web.settings.title")}
/>
<div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]"> <div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<aside className="rounded-xl border border-border bg-card p-4 shadow-sm lg:sticky lg:top-6 lg:self-start"> <aside className="rounded-xl border border-border bg-card p-4 shadow-sm lg:sticky lg:top-6 lg:self-start">
<SettingsNavigation sections={sections} /> <SettingsNavigation sections={sections} />
</aside> </aside>
<section className="min-w-0"> <section className="min-w-0">
<Outlet /> <Outlet />
</section> </section>
</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,68 +34,72 @@ export const AuditPage = () => {
if (controller.isError && !controller.hasUsableData) { if (controller.isError && !controller.hasUsableData) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<PageErrorState <RdxPageContainer width="content">
description={t("audit.error.load")} <PageErrorState
primaryAction={{ description={t("audit.error.load")}
label: t("audit.retry"), primaryAction={{
onClick: retry, label: t("audit.retry"),
isLoading: controller.isFetching, onClick: retry,
}} isLoading: controller.isFetching,
title={t("audit.error.load")} }}
variant="unknown" title={t("audit.error.load")}
/> variant="unknown"
</RdxPageContainer> />
</RdxPageContainer>
</RdxPageFrame>
); );
} }
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageStack spacing="default"> <RdxPageContainer width="full">
<RdxPageHeader description={t("audit.description")} title={t("audit.title")} /> <RdxPageStack spacing="default">
<RdxPageHeader description={t("audit.description")} title={t("audit.title")} />
{controller.isError && controller.hasUsableData ? (
<ErrorAlert message={t("audit.error.refresh")} title={t("audit.error.load")} /> {controller.isError && controller.hasUsableData ? (
) : null} <ErrorAlert message={t("audit.error.refresh")} title={t("audit.error.load")} />
) : null}
<div className="flex flex-col gap-4 lg:flex-row">
<AuditFiltersPanel filters={controller.filters} onChange={controller.setFilters} /> <div className="flex flex-col gap-4 lg:flex-row">
<div className="min-w-0 flex-1 space-y-4"> <AuditFiltersPanel filters={controller.filters} onChange={controller.setFilters} />
<AuditToolbar controller={controller} /> <div className="min-w-0 flex-1 space-y-4">
<AuditFilterChips <AuditToolbar controller={controller} />
filters={controller.filters} <AuditFilterChips
onChange={controller.setFilters} filters={controller.filters}
onReset={controller.resetFilters} onChange={controller.setFilters}
/> onReset={controller.resetFilters}
{!controller.isLoading && controller.data.items.length === 0 ? (
<p className="text-sm text-muted-foreground" role="status">
{controller.filters.q ||
controller.filters.kind ||
controller.filters.targetType ||
controller.filters.action
? t("audit.empty_filtered")
: t("audit.empty")}
</p>
) : (
<AuditGrid
columns={columns}
data={controller.data}
fetching={controller.isFetching}
loading={controller.isLoading}
onOrderChange={controller.setOrder}
onPageChange={controller.setPageNumber}
onPageSizeChange={controller.setPageSize}
onRowClick={controller.selectEntry}
order={controller.order}
pageNumber={controller.pageNumber}
pageSize={controller.pageSize}
/> />
)} {!controller.isLoading && controller.data.items.length === 0 ? (
<p className="text-sm text-muted-foreground" role="status">
{controller.filters.q ||
controller.filters.kind ||
controller.filters.targetType ||
controller.filters.action
? t("audit.empty_filtered")
: t("audit.empty")}
</p>
) : (
<AuditGrid
columns={columns}
data={controller.data}
fetching={controller.isFetching}
loading={controller.isLoading}
onOrderChange={controller.setOrder}
onPageChange={controller.setPageNumber}
onPageSizeChange={controller.setPageSize}
onRowClick={controller.selectEntry}
order={controller.order}
pageNumber={controller.pageNumber}
pageSize={controller.pageSize}
/>
)}
</div>
</div> </div>
</div> <AuditEntrySheet entryId={controller.entry} onClose={controller.closeEntry} />
<AuditEntrySheet entryId={controller.entry} onClose={controller.closeEntry} /> </RdxPageStack>
</RdxPageStack> </RdxPageContainer>
</RdxPageContainer> </RdxPageFrame>
); );
}; };

View File

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

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,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,6 +1,10 @@
import { PageFormHeader } from "@erp/core/components";
import { useHookForm } from "@erp/core/hooks"; import { useHookForm } from "@erp/core/hooks";
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components"; import {
RdxEditorHeader,
RdxEditorLayout,
RdxPageContainer,
RdxPageFrame,
} from "@repo/rdx-ui/components";
import { import {
Button, Button,
Card, Card,
@ -53,7 +57,7 @@ export const RecurringProformaCreatePage = () => {
<RdxPageFrame <RdxPageFrame
contentClassName="bg-muted/20" contentClassName="bg-muted/20"
header={ header={
<PageFormHeader <RdxEditorHeader
actions={ actions={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button

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,32 +19,35 @@ 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
<CustomerCreateHeader contentClassName="bg-muted/20"
formId={createCtrl.formId} header={
hasChanges={createCtrl.form.formState.isDirty} <CustomerCreateHeader
isSaving={createCtrl.form.formState.isSubmitting} formId={createCtrl.formId}
labels={{ hasChanges={createCtrl.form.formState.isDirty}
title: t("create.title"), isSaving={createCtrl.form.formState.isSubmitting}
back: t("create.header.back"), labels={{
modified: t("create.header.modified"), title: t("create.title"),
cancel: t("common.actions.cancel"), back: t("create.header.back"),
save: t("common.actions.save"), modified: t("create.header.modified"),
saving: t("common.actions.saving"), cancel: t("common.actions.cancel"),
moreActions: t("common.actions.more"), save: t("common.actions.save"),
keyboardShortcuts: t("create.header.keyboard_shortcuts"), saving: t("common.actions.saving"),
delete: t("create.header.delete"), moreActions: t("common.actions.more"),
}} keyboardShortcuts: t("create.header.keyboard_shortcuts"),
onCancel={handleCancel} delete: t("create.header.delete"),
//onDelete={openDeleteDialog} }}
//onDuplicate={handleDuplicate} onCancel={handleCancel}
//onExportPdf={handleExportPdf} //onDelete={openDeleteDialog}
/> //onDuplicate={handleDuplicate}
//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>
</UnsavedChangesProvider> </RdxPageFrame>
</FormProvider> </UnsavedChangesProvider>
</div> </FormProvider>
); );
}; };

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,22 +31,24 @@ export const ListCustomersPageView = ({
dialogs, dialogs,
}: ListCustomersPageViewProps) => { }: ListCustomersPageViewProps) => {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageStack spacing="default"> <RdxPageContainer width="full">
<RdxPageHeader <RdxPageStack spacing="default">
actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined} <RdxPageHeader
description={description} actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined}
title={title} description={description}
/> title={title}
/>
{refreshError ? ( {refreshError ? (
<ErrorAlert message={refreshError.message} title={refreshError.title} /> <ErrorAlert message={refreshError.message} title={refreshError.title} />
) : null} ) : null}
{content} {content}
</RdxPageStack> </RdxPageStack>
{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>
); );
} }
@ -157,14 +158,14 @@ export const ListCustomersPage = () => {
} }
description={t("list.description")} description={t("list.description")}
primaryAction={ primaryAction={
<Button <Button
aria-label={t("common.actions.create")} aria-label={t("common.actions.create")}
onClick={() => navigate("/customers/create")} onClick={() => navigate("/customers/create")}
size="default" size="default"
> >
<PlusIcon aria-hidden className="mr-2 size-4" /> <PlusIcon aria-hidden className="mr-2 size-4" />
{t("common.actions.create")} {t("common.actions.create")}
</Button> </Button>
} }
refreshError={refreshError} refreshError={refreshError}
title={t("list.title")} title={t("list.title")}

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,39 +61,42 @@ 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
<CustomerUpdateHeader contentClassName="bg-muted/20"
formId={updateCtrl.formId} header={
hasChanges={updateCtrl.form.formState.isDirty} <CustomerUpdateHeader
isSaving={updateCtrl.form.formState.isSubmitting} formId={updateCtrl.formId}
labels={{ hasChanges={updateCtrl.form.formState.isDirty}
title: "Editar cliente", isSaving={updateCtrl.form.formState.isSubmitting}
back: "Volver", labels={{
modified: "Modificada", title: "Editar cliente",
cancel: "Cancelar", back: "Volver",
save: "Guardar", modified: "Modificada",
saving: "Guardando...", cancel: "Cancelar",
moreActions: "Más acciones", save: "Guardar",
keyboardShortcuts: "Ver atajos de teclado", saving: "Guardando...",
duplicate: "Duplicar cliente", moreActions: "Más acciones",
delete: "Eliminar", keyboardShortcuts: "Ver atajos de teclado",
}} duplicate: "Duplicar cliente",
onCancel={handleCancel} delete: "Eliminar",
//onDelete={openDeleteDialog} }}
//onDuplicate={handleDuplicate} onCancel={handleCancel}
//onExportPdf={handleExportPdf} //onDelete={openDeleteDialog}
/> //onDuplicate={handleDuplicate}
//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>
</UnsavedChangesProvider> </RdxPageFrame>
</FormProvider> </UnsavedChangesProvider>
</div> </FormProvider>
); );
}; };

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,32 +71,34 @@ 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
<CustomerViewHeader header={
labels={{ <CustomerViewHeader
title: "Ver cliente", labels={{
back: "Volver", title: "Ver cliente",
modified: "Modificada", back: "Volver",
cancel: "Cancelar", modified: "Modificada",
save: "Guardar", cancel: "Cancelar",
saving: "Guardando...", save: "Guardar",
moreActions: "Más acciones", saving: "Guardando...",
keyboardShortcuts: "Ver atajos de teclado", moreActions: "Más acciones",
delete: "Eliminar", keyboardShortcuts: "Ver atajos de teclado",
}} delete: "Eliminar",
onCancel={handleCancel} }}
//onDelete={openDeleteDialog} onCancel={handleCancel}
//onDuplicate={handleDuplicate} //onDelete={openDeleteDialog}
//onExportPdf={handleExportPdf} //onDuplicate={handleDuplicate}
/> //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

@ -1,8 +1,9 @@
import { ErrorAlert, PageErrorState, PageFormHeader } 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 { import {
CancelActionButton, CancelActionButton,
RdxEditorActionBar, RdxEditorActionBar,
RdxEditorHeader,
RdxEditorPageLayout, RdxEditorPageLayout,
RdxPanel, RdxPanel,
RhfSubmitActionButton, RhfSubmitActionButton,
@ -252,7 +253,7 @@ export const AccountPage = () => {
return ( return (
<div className="fixed inset-0 flex flex-col overflow-hidden"> <div className="fixed inset-0 flex flex-col overflow-hidden">
<PageFormHeader <RdxEditorHeader
backLabel={t("account.header.back")} backLabel={t("account.header.back")}
onBack={navigateBack} onBack={navigateBack}
title={t("account.title")} title={t("account.title")}

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,18 +24,22 @@ 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
<SupplierEditorHeader contentClassName="bg-muted/20"
formId={controller.formId} header={
hasChanges={controller.form.formState.isDirty} <SupplierEditorHeader
isSaving={controller.mutation.isPending} formId={controller.formId}
onCancel={navigateBack} hasChanges={controller.form.formState.isDirty}
title={t("create.title")} isSaving={controller.mutation.isPending}
/> onCancel={navigateBack}
<div className="flex-1 overflow-y-auto bg-muted/20"> title={t("create.title")}
<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>
</UnsavedChangesProvider> </RdxPageFrame>
</FormProvider> </UnsavedChangesProvider>
</div> </FormProvider>
); );
}; };

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,63 +37,67 @@ 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>
<PageErrorState <RdxPageContainer width="content">
description={t("list.error.description")} <PageErrorState
primaryAction={{ description={t("list.error.description")}
label: t("common.actions.retry"), primaryAction={{
onClick: async () => { label: t("common.actions.retry"),
await controller.refetch(); onClick: async () => {
}, await controller.refetch();
}} },
title={t("list.error.title")} }}
variant="unknown" title={t("list.error.title")}
/> variant="unknown"
</RdxPageContainer> />
</RdxPageContainer>
</RdxPageFrame>
); );
} }
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<RdxPageStack spacing="default"> <RdxPageContainer width="full">
<RdxPageHeader <RdxPageStack spacing="default">
actions={ <RdxPageHeader
<RdxPageActions actions={
primary={ <RdxPageActions
<Button onClick={() => navigate("/suppliers/create")}> primary={
<PlusIcon aria-hidden className="mr-2 size-4" /> <Button onClick={() => navigate("/suppliers/create")}>
{t("common.actions.create")} <PlusIcon aria-hidden className="mr-2 size-4" />
</Button> {t("common.actions.create")}
} </Button>
/> }
} />
description={t("list.description")} }
title={t("list.title")} description={t("list.description")}
/> title={t("list.title")}
<SuppliersGrid />
columns={columns} <SuppliersGrid
columnVisibility={controller.tablePreferences.columnVisibility} columns={columns}
data={controller.data} columnVisibility={controller.tablePreferences.columnVisibility}
fetching={controller.isFetching} data={controller.data}
loading={controller.isLoading} fetching={controller.isFetching}
onColumnVisibilityChange={controller.tablePreferences.setColumnVisibility} loading={controller.isLoading}
onPageChange={controller.setPageIndex} onColumnVisibilityChange={controller.tablePreferences.setColumnVisibility}
onPageSizeChange={controller.setPageSize} onPageChange={controller.setPageIndex}
onSortChange={controller.setSort} onPageSizeChange={controller.setPageSize}
pageIndex={controller.pageIndex} onSortChange={controller.setSort}
pageSize={controller.pageSize} pageIndex={controller.pageIndex}
searching={isSearchPending} pageSize={controller.pageSize}
sort={controller.sort} searching={isSearchPending}
topContent={ sort={controller.sort}
<SimpleSearchInput topContent={
loading={controller.isFetching} <SimpleSearchInput
onPendingChange={setIsSearchPending} loading={controller.isFetching}
onSearchChange={controller.setSearch} onPendingChange={setIsSearchPending}
value={controller.search} onSearchChange={controller.setSearch}
/> value={controller.search}
} />
/> }
</RdxPageStack> />
</RdxPageContainer> </RdxPageStack>
</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,16 +17,18 @@ export const SupplierEditorSkeleton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<AppContent> <RdxPageFrame>
<div aria-hidden className="space-y-6"> <RdxPageContainer width="content">
<div className="h-10 w-64 animate-pulse rounded-md bg-muted" /> <div aria-hidden className="space-y-6">
<div className="grid gap-4 md:grid-cols-2"> <div className="h-10 w-64 animate-pulse rounded-md bg-muted" />
{SKELETON_FIELDS.map((field) => ( <div className="grid gap-4 md:grid-cols-2">
<div className="h-16 animate-pulse rounded-md bg-muted" key={field} /> {SKELETON_FIELDS.map((field) => (
))} <div className="h-16 animate-pulse rounded-md bg-muted" key={field} />
))}
</div>
</div> </div>
</div> <span className="sr-only">{t("common.loading")}</span>
<span className="sr-only">{t("common.loading")}</span> </RdxPageContainer>
</AppContent> </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,36 +28,42 @@ export const SupplierUpdatePage = () => {
if (controller.query.isError || !controller.query.data) { if (controller.query.isError || !controller.query.data) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<PageErrorState <RdxPageContainer width="content">
description={t("detail.error.description")} <PageErrorState
primaryAction={{ description={t("detail.error.description")}
label: t("common.actions.retry"), primaryAction={{
onClick: async () => { label: t("common.actions.retry"),
await controller.query.refetch(); onClick: async () => {
}, await controller.query.refetch();
}} },
secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }} }}
title={t("detail.error.title")} secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }}
variant="notFound" title={t("detail.error.title")}
/> variant="notFound"
</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
<SupplierEditorHeader contentClassName="bg-muted/20"
formId={controller.formId} header={
hasChanges={controller.form.formState.isDirty} <SupplierEditorHeader
isSaving={controller.mutation.isPending} formId={controller.formId}
onCancel={navigateBack} hasChanges={controller.form.formState.isDirty}
title={t("update.title")} isSaving={controller.mutation.isPending}
/> onCancel={navigateBack}
<div className="flex-1 overflow-y-auto bg-muted/20"> title={t("update.title")}
<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>
</UnsavedChangesProvider> </RdxPageFrame>
</FormProvider> </UnsavedChangesProvider>
</div> </FormProvider>
); );
}; };

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,20 +36,22 @@ export const SupplierDetailPage = () => {
if (query.isError || !query.data) { if (query.isError || !query.data) {
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame>
<PageErrorState <RdxPageContainer width="content">
description={t("detail.error.description")} <PageErrorState
primaryAction={{ description={t("detail.error.description")}
label: t("common.actions.retry"), primaryAction={{
onClick: async () => { label: t("common.actions.retry"),
await query.refetch(); onClick: async () => {
}, await query.refetch();
}} },
secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }} }}
title={t("detail.error.title")} secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }}
variant="notFound" title={t("detail.error.title")}
/> variant="notFound"
</RdxPageContainer> />
</RdxPageContainer>
</RdxPageFrame>
); );
} }
@ -56,121 +59,133 @@ export const SupplierDetailPage = () => {
const context = supplier.reference || supplier.tin || supplier.tradeName || "-"; const context = supplier.reference || supplier.tin || supplier.tradeName || "-";
return ( return (
<RdxPageContainer width="full"> <RdxPageFrame
<RdxPageStack spacing="default"> header={
<RdxPageHeader <RdxPageContainer width="full">
actions={ <RdxPageHeader
<RdxPageActions actions={
primary={ <RdxPageActions
<Button primary={
onClick={() => <Button
navigate({ onClick={() =>
pathname: `/suppliers/${supplier.id}/edit`, navigate({
search: createSearchParams({ returnTo }).toString(), pathname: `/suppliers/${supplier.id}/edit`,
}) search: createSearchParams({ returnTo }).toString(),
} })
> }
<PencilIcon aria-hidden className="mr-2 size-4" /> >
{t("common.actions.edit")} <PencilIcon aria-hidden className="mr-2 size-4" />
</Button> {t("common.actions.edit")}
} </Button>
secondary={ }
<Button onClick={navigateBack} variant="outline"> secondary={
{t("common.actions.back")} <Button onClick={navigateBack} variant="outline">
</Button> {t("common.actions.back")}
} </Button>
/> }
} />
description={context} }
title={ description={context}
<span className="flex flex-wrap items-center gap-3"> title={
{supplier.name} <span className="flex flex-wrap items-center gap-3">
<SupplierStatusBadge status={supplier.status} /> {supplier.name}
</span> <SupplierStatusBadge status={supplier.status} />
} </span>
/> }
/>
</RdxPageContainer>
}
>
<RdxPageContainer width="full">
<RdxPageStack spacing="default">
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<FileTextIcon aria-hidden className="size-5 text-primary" />
{t("detail.sections.identity")}
</CardTitle>
</CardHeader>
<CardContent>
<dl className="grid gap-4 sm:grid-cols-2">
<DetailField label={t("fields.name")} value={supplier.name} />
<DetailField label={t("fields.trade_name")} value={supplier.tradeName} />
<DetailField label={t("fields.reference")} value={supplier.reference} />
<DetailField label={t("fields.tin")} value={supplier.tin} />
<DetailField
label={t("fields.supplier_type")}
value={t(supplier.isCompany ? "fields.company" : "fields.individual")}
/>
</dl>
</CardContent>
</Card>
<div className="grid gap-4 lg:grid-cols-2"> <Card>
<Card> <CardHeader>
<CardHeader> <CardTitle className="flex items-center gap-2 text-lg">
<CardTitle className="flex items-center gap-2 text-lg"> <MailIcon aria-hidden className="size-5 text-primary" />
<FileTextIcon aria-hidden className="size-5 text-primary" /> {t("detail.sections.contact")}
{t("detail.sections.identity")} </CardTitle>
</CardTitle> </CardHeader>
</CardHeader> <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.name")} value={supplier.name} /> <DetailField
<DetailField label={t("fields.trade_name")} value={supplier.tradeName} /> label={t("fields.secondary_email")}
<DetailField label={t("fields.reference")} value={supplier.reference} /> value={supplier.secondaryEmail}
<DetailField label={t("fields.tin")} value={supplier.tin} /> />
<DetailField <DetailField label={t("fields.primary_phone")} value={supplier.primaryPhone} />
label={t("fields.supplier_type")} <DetailField
value={t(supplier.isCompany ? "fields.company" : "fields.individual")} label={t("fields.secondary_phone")}
/> value={supplier.secondaryPhone}
</dl> />
</CardContent> <DetailField label={t("fields.primary_mobile")} value={supplier.primaryMobile} />
</Card> <DetailField
label={t("fields.secondary_mobile")}
value={supplier.secondaryMobile}
/>
<DetailField label={t("fields.fax")} value={supplier.fax} />
<DetailField label={t("fields.website")} value={supplier.website} />
</dl>
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="flex items-center gap-2 text-lg">
<MailIcon aria-hidden className="size-5 text-primary" /> <MapPinIcon aria-hidden className="size-5 text-primary" />
{t("detail.sections.contact")} {t("detail.sections.address")}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<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.street")} value={supplier.street} />
<DetailField label={t("fields.secondary_email")} value={supplier.secondaryEmail} /> <DetailField label={t("fields.street2")} value={supplier.street2} />
<DetailField label={t("fields.primary_phone")} value={supplier.primaryPhone} /> <DetailField label={t("fields.city")} value={supplier.city} />
<DetailField label={t("fields.secondary_phone")} value={supplier.secondaryPhone} /> <DetailField label={t("fields.postal_code")} value={supplier.postalCode} />
<DetailField label={t("fields.primary_mobile")} value={supplier.primaryMobile} /> <DetailField label={t("fields.province")} value={supplier.province} />
<DetailField <DetailField label={t("fields.country")} value={supplier.country} />
label={t("fields.secondary_mobile")} </dl>
value={supplier.secondaryMobile} </CardContent>
/> </Card>
<DetailField label={t("fields.fax")} value={supplier.fax} />
<DetailField label={t("fields.website")} value={supplier.website} />
</dl>
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="flex items-center gap-2 text-lg">
<MapPinIcon aria-hidden className="size-5 text-primary" /> <LanguagesIcon aria-hidden className="size-5 text-primary" />
{t("detail.sections.address")} {t("detail.sections.preferences")}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<dl className="grid gap-4 sm:grid-cols-2"> <dl className="grid gap-4 sm:grid-cols-2">
<DetailField label={t("fields.street")} value={supplier.street} /> <DetailField label={t("fields.language")} value={supplier.languageCode} />
<DetailField label={t("fields.street2")} value={supplier.street2} /> <DetailField label={t("fields.currency")} value={supplier.currencyCode} />
<DetailField label={t("fields.city")} value={supplier.city} /> </dl>
<DetailField label={t("fields.postal_code")} value={supplier.postalCode} /> </CardContent>
<DetailField label={t("fields.province")} value={supplier.province} /> </Card>
<DetailField label={t("fields.country")} value={supplier.country} /> </div>
</dl> </RdxPageStack>
</CardContent> </RdxPageContainer>
</Card> </RdxPageFrame>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<LanguagesIcon aria-hidden className="size-5 text-primary" />
{t("detail.sections.preferences")}
</CardTitle>
</CardHeader>
<CardContent>
<dl className="grid gap-4 sm:grid-cols-2">
<DetailField label={t("fields.language")} value={supplier.languageCode} />
<DetailField label={t("fields.currency")} value={supplier.currencyCode} />
</dl>
</CardContent>
</Card>
</div>
</RdxPageStack>
</RdxPageContainer>
); );
}; };

View File

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

View File

@ -6,6 +6,7 @@ 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 { RdxEditorHeader } from "./rdx-editor-header.tsx";
import { RdxEditorLayout } from "./rdx-editor-layout.tsx"; import { RdxEditorLayout } from "./rdx-editor-layout.tsx";
import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx"; import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx";
@ -20,6 +21,33 @@ test("RdxEditorLayout only distributes main content and an optional aside", () =
assert.doesNotMatch(html, /px-4/); 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", () => { test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza children", () => {

View File

@ -1,74 +1,75 @@
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">
<Button {onBack ? (
aria-label={backLabel} <Button
disabled={!onBack} aria-label={backLabel}
onClick={onBack} onClick={onBack}
size="icon" size="icon"
type="button" type="button"
variant="ghost" variant="ghost"
> >
<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

@ -106,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

@ -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(