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.
This commit is contained in:
parent
3d2ce8b981
commit
3eb7c37a7b
@ -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 = () => {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
const ProformaLayout = lazy(() =>
|
|
||||||
import("./proformas/shared/ui").then((m) => ({ default: m.ProformaLayout }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const RecurringProformaLayout = lazy(() =>
|
const RecurringProformaLayout = lazy(() =>
|
||||||
import("./recurring-proformas/shared/ui").then((m) => ({ default: m.RecurringProformaLayout }))
|
import("./recurring-proformas/shared/ui").then((m) => ({ default: m.RecurringProformaLayout }))
|
||||||
);
|
);
|
||||||
@ -70,11 +66,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 +93,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 +102,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 +111,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",
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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" },
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,26 +21,25 @@ 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
|
||||||
<ProformaCreateHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={createCtrl.formId}
|
header={
|
||||||
isSaving={createCtrl.isCreating}
|
<ProformaCreateHeader
|
||||||
onCancel={navigateBack}
|
formId={createCtrl.formId}
|
||||||
>
|
isSaving={createCtrl.isCreating}
|
||||||
<ProformaInfoAlert>
|
onCancel={navigateBack}
|
||||||
<span className="font-semibold">Importante: </span>Las proformas no tienen validez
|
>
|
||||||
fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente.
|
<ProformaInfoAlert>
|
||||||
</ProformaInfoAlert>
|
<span className="font-semibold">Importante: </span>Las proformas no tienen validez
|
||||||
</ProformaCreateHeader>
|
fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente.
|
||||||
|
</ProformaInfoAlert>
|
||||||
<RdxPageContainer
|
</ProformaCreateHeader>
|
||||||
className="min-h-0 flex-1 overflow-y-auto bg-muted/20"
|
}
|
||||||
padding="none"
|
>
|
||||||
width="full"
|
<RdxPageContainer width="content">
|
||||||
>
|
<RdxEditorLayout>
|
||||||
<RdxEditorPageLayout width="simple">
|
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageStack spacing="default">
|
||||||
{createCtrl.isCreateError && (
|
{createCtrl.isCreateError && (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
@ -75,16 +79,16 @@ export const ProformaCreatePage = () => {
|
|||||||
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
|
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
|
||||||
/>
|
/>
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
|
|
||||||
<SelectCustomerDialog
|
<SelectCustomerDialog
|
||||||
ctrl={selectCustomerCtrl.selectCtrl}
|
ctrl={selectCustomerCtrl.selectCtrl}
|
||||||
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
||||||
/>
|
/>
|
||||||
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</RdxPageStack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
<ProformaDetailHeader
|
contentClassName="lg:overflow-hidden"
|
||||||
actions={ctrl.actions}
|
header={
|
||||||
header={ctrl.header}
|
<ProformaDetailHeader
|
||||||
onBack={ctrl.navigation.navigateBack}
|
actions={ctrl.actions}
|
||||||
onExecuteAction={ctrl.handlers.executeAction}
|
header={ctrl.header}
|
||||||
/>
|
onBack={ctrl.navigation.navigateBack}
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 ScopedListProformasPageView = ({
|
|||||||
dialogs,
|
dialogs,
|
||||||
}: ScopedListProformasPageViewProps) => {
|
}: ScopedListProformasPageViewProps) => {
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,178 +95,174 @@ 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
|
||||||
<ProformaUpdateHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={updateCtrl.formId}
|
footer={actionBar}
|
||||||
hasChanges={updateCtrl.form.formState.isDirty}
|
header={
|
||||||
helpAction={
|
<ProformaUpdateHeader
|
||||||
<ProformaUpdateHelpPopover
|
formId={updateCtrl.formId}
|
||||||
isDirty={updateCtrl.form.formState.isDirty}
|
hasChanges={updateCtrl.form.formState.isDirty}
|
||||||
isReadinessLoading={ctrl.isIssuanceReadinessLoading}
|
helpAction={
|
||||||
onRetryReadiness={handleRetryIssuanceReadiness}
|
<ProformaUpdateHelpPopover
|
||||||
readiness={ctrl.issuanceReadiness}
|
isDirty={updateCtrl.form.formState.isDirty}
|
||||||
readinessError={ctrl.issuanceReadinessError}
|
isReadinessLoading={ctrl.isIssuanceReadinessLoading}
|
||||||
/>
|
onRetryReadiness={handleRetryIssuanceReadiness}
|
||||||
}
|
readiness={ctrl.issuanceReadiness}
|
||||||
isSaving={updateCtrl.form.formState.isSubmitting}
|
readinessError={ctrl.issuanceReadinessError}
|
||||||
labels={{
|
|
||||||
title: proforma ? (
|
|
||||||
<span className="inline-flex max-w-full items-center gap-3">
|
|
||||||
<span>{t("pages.proformas.update.title", "Editar proforma")}</span>
|
|
||||||
<RdxBadge className="border-border/80 bg-muted text-muted-foreground">
|
|
||||||
{proforma.proformaReference}
|
|
||||||
</RdxBadge>
|
|
||||||
<ProformaStatusBadge status={proforma.status} />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
t("pages.proformas.update.title", "Editar proforma")
|
|
||||||
),
|
|
||||||
back: "Volver",
|
|
||||||
modified: "Modificada",
|
|
||||||
cancel: "Cancelar",
|
|
||||||
save: "Guardar",
|
|
||||||
saving: "Guardando...",
|
|
||||||
moreActions: "Más acciones",
|
|
||||||
keyboardShortcuts: "Ver atajos de teclado",
|
|
||||||
duplicate: "Duplicar proforma",
|
|
||||||
exportPdf: "Exportar PDF",
|
|
||||||
delete: "Eliminar",
|
|
||||||
}}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
//onDelete={openDeleteDialog}
|
|
||||||
//onDuplicate={handleDuplicate}
|
|
||||||
//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"
|
|
||||||
>
|
|
||||||
{hasPageAlerts ? (
|
|
||||||
<RdxPageContainer className="pb-0" width="full">
|
|
||||||
<RdxPageStack spacing="compact">
|
|
||||||
{updateCtrl.isUpdateError && (
|
|
||||||
<ErrorAlert
|
|
||||||
message={
|
|
||||||
(updateCtrl.updateError as Error)?.message ??
|
|
||||||
t(
|
|
||||||
"pages.proformas.update.error_msg",
|
|
||||||
"Revisa los datos e inténtalo de nuevo."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
title={t(
|
|
||||||
"pages.proformas.update.error_title",
|
|
||||||
"No se pudo guardar los cambios"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updateCtrl.isSeriesLoadError && (
|
|
||||||
<ErrorAlert
|
|
||||||
message={
|
|
||||||
updateCtrl.seriesLoadError instanceof Error
|
|
||||||
? updateCtrl.seriesLoadError.message
|
|
||||||
: t(
|
|
||||||
"pages.proformas.update.series_load_error.message",
|
|
||||||
"Inténtalo de nuevo más tarde."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
title={t(
|
|
||||||
"pages.proformas.update.series_load_error.title",
|
|
||||||
"No se pudieron cargar las series de facturación"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</RdxPageStack>
|
|
||||||
</RdxPageContainer>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<ProformaUpdateEditorForm
|
|
||||||
className={hasPageAlerts ? "pt-0" : undefined}
|
|
||||||
formId={updateCtrl.formId}
|
|
||||||
isSubmitting={updateCtrl.isUpdating}
|
|
||||||
itemsCtrl={updateCtrl.itemsCtrl}
|
|
||||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
|
||||||
onCreateCustomerClick={selectCustomerCtrl.openQuickCreateDialog}
|
|
||||||
onPaymentTermChange={updateCtrl.handlePaymentTermChange}
|
|
||||||
onReset={updateCtrl.resetForm}
|
|
||||||
onSubmit={updateCtrl.onSubmit}
|
|
||||||
paymentCtrl={updateCtrl.paymentCtrl}
|
|
||||||
selectedCustomer={updateCtrl.selectedCustomer}
|
|
||||||
seriesOptions={updateCtrl.seriesOptions}
|
|
||||||
taxCtrl={updateCtrl.taxCtrl}
|
|
||||||
totalsCtrl={updateCtrl.totalsCtrl}
|
|
||||||
/>
|
|
||||||
</RdxPageContainer>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
isSaving={updateCtrl.form.formState.isSubmitting}
|
||||||
|
labels={{
|
||||||
|
title: proforma ? (
|
||||||
|
<span className="inline-flex max-w-full items-center gap-3">
|
||||||
|
<span>{t("pages.proformas.update.title", "Editar proforma")}</span>
|
||||||
|
<RdxBadge className="border-border/80 bg-muted text-muted-foreground">
|
||||||
|
{proforma.proformaReference}
|
||||||
|
</RdxBadge>
|
||||||
|
<ProformaStatusBadge status={proforma.status} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
t("pages.proformas.update.title", "Editar proforma")
|
||||||
|
),
|
||||||
|
back: "Volver",
|
||||||
|
modified: "Modificada",
|
||||||
|
cancel: "Cancelar",
|
||||||
|
save: "Guardar",
|
||||||
|
saving: "Guardando...",
|
||||||
|
moreActions: "Más acciones",
|
||||||
|
keyboardShortcuts: "Ver atajos de teclado",
|
||||||
|
duplicate: "Duplicar proforma",
|
||||||
|
exportPdf: "Exportar PDF",
|
||||||
|
delete: "Eliminar",
|
||||||
|
}}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
//onDelete={openDeleteDialog}
|
||||||
|
//onDuplicate={handleDuplicate}
|
||||||
|
//onExportPdf={handleExportPdf}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer padding="default" width="full">
|
||||||
|
{hasPageAlerts ? (
|
||||||
|
<RdxPageContainer className="pb-0" width="full">
|
||||||
|
<RdxPageStack spacing="compact">
|
||||||
|
{updateCtrl.isUpdateError && (
|
||||||
|
<ErrorAlert
|
||||||
|
message={
|
||||||
|
(updateCtrl.updateError as Error)?.message ??
|
||||||
|
t(
|
||||||
|
"pages.proformas.update.error_msg",
|
||||||
|
"Revisa los datos e inténtalo de nuevo."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={t(
|
||||||
|
"pages.proformas.update.error_title",
|
||||||
|
"No se pudo guardar los cambios"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SelectCustomerDialog
|
{updateCtrl.isSeriesLoadError && (
|
||||||
ctrl={selectCustomerCtrl.selectCtrl}
|
<ErrorAlert
|
||||||
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
message={
|
||||||
/>
|
updateCtrl.seriesLoadError instanceof Error
|
||||||
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
? updateCtrl.seriesLoadError.message
|
||||||
<ProformaUpdateCustomerChangeDialog
|
: t(
|
||||||
evaluation={customerChangeCtrl.evaluation}
|
"pages.proformas.update.series_load_error.message",
|
||||||
isSubmitting={customerChangeCtrl.isApplying}
|
"Inténtalo de nuevo más tarde."
|
||||||
loadError={customerChangeCtrl.loadError as Error | null}
|
)
|
||||||
onConfirm={customerChangeCtrl.confirmChange}
|
}
|
||||||
onOpenChange={(open) => {
|
title={t(
|
||||||
if (!open) {
|
"pages.proformas.update.series_load_error.title",
|
||||||
customerChangeCtrl.closeDialog();
|
"No se pudieron cargar las series de facturación"
|
||||||
}
|
)}
|
||||||
}}
|
/>
|
||||||
open={customerChangeCtrl.open}
|
)}
|
||||||
status={customerChangeCtrl.candidateStatus}
|
</RdxPageStack>
|
||||||
/>
|
</RdxPageContainer>
|
||||||
</UnsavedChangesProvider>
|
) : null}
|
||||||
</FormProvider>
|
|
||||||
</RdxPageStack>
|
<ProformaUpdateEditorForm
|
||||||
|
className={hasPageAlerts ? "pt-0" : undefined}
|
||||||
|
formId={updateCtrl.formId}
|
||||||
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
|
itemsCtrl={updateCtrl.itemsCtrl}
|
||||||
|
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
|
onCreateCustomerClick={selectCustomerCtrl.openQuickCreateDialog}
|
||||||
|
onPaymentTermChange={updateCtrl.handlePaymentTermChange}
|
||||||
|
onReset={updateCtrl.resetForm}
|
||||||
|
onSubmit={updateCtrl.onSubmit}
|
||||||
|
paymentCtrl={updateCtrl.paymentCtrl}
|
||||||
|
selectedCustomer={updateCtrl.selectedCustomer}
|
||||||
|
seriesOptions={updateCtrl.seriesOptions}
|
||||||
|
taxCtrl={updateCtrl.taxCtrl}
|
||||||
|
totalsCtrl={updateCtrl.totalsCtrl}
|
||||||
|
/>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
|
|
||||||
|
<SelectCustomerDialog
|
||||||
|
ctrl={selectCustomerCtrl.selectCtrl}
|
||||||
|
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
||||||
|
/>
|
||||||
|
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
||||||
|
<ProformaUpdateCustomerChangeDialog
|
||||||
|
evaluation={customerChangeCtrl.evaluation}
|
||||||
|
isSubmitting={customerChangeCtrl.isApplying}
|
||||||
|
loadError={customerChangeCtrl.loadError as Error | null}
|
||||||
|
onConfirm={customerChangeCtrl.confirmChange}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
customerChangeCtrl.closeDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
open={customerChangeCtrl.open}
|
||||||
|
status={customerChangeCtrl.candidateStatus}
|
||||||
|
/>
|
||||||
|
</UnsavedChangesProvider>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -133,7 +133,7 @@ export const RecurringProformaDetailPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<RdxPageContainer className="min-h-0 flex-1 overflow-y-auto" padding="none" width="wide">
|
<RdxPageContainer className="min-h-0 flex-1 overflow-y-auto" padding="none" width="wide">
|
||||||
<RdxEditorPageLayout width="simple">
|
<RdxEditorPageLayout width="wide">
|
||||||
<RecurringProformaOperationalSummary
|
<RecurringProformaOperationalSummary
|
||||||
locale={dateLocale}
|
locale={dateLocale}
|
||||||
recurringProforma={recurringProforma}
|
recurringProforma={recurringProforma}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./rdx-editor-action-bar.tsx";
|
export * from "./rdx-editor-action-bar.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";
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -6,7 +6,20 @@ 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 { RdxEditorLayout } from "./rdx-editor-layout.tsx";
|
||||||
import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx";
|
import { RdxEditorPageLayout } from "./rdx-editor-page-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/);
|
||||||
|
});
|
||||||
|
|
||||||
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", () => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,34 +50,30 @@ 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">
|
{icon ? (
|
||||||
{icon ? (
|
<div className="flex size-6 shrink-0 items-center justify-center text-primary">
|
||||||
<div className="flex size-6 shrink-0 items-center justify-center text-primary">
|
{icon}
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="min-w-0 space-y-1">
|
|
||||||
<h2
|
|
||||||
className="min-w-0 text-base font-semibold tracking-tight text-foreground"
|
|
||||||
id={titleId}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{description ? (
|
|
||||||
<p className="text-sm text-muted-foreground">{description}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
<div className="min-w-0 space-y-1">
|
||||||
|
<h2
|
||||||
|
className="min-w-0 text-base font-semibold tracking-tight text-foreground"
|
||||||
|
id={titleId}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0">{children}</div>
|
<div className="min-w-0">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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>);
|
||||||
|
|
||||||
|
|||||||
35
packages/rdx-ui/src/components/layout/rdx-page-frame.tsx
Normal file
35
packages/rdx-ui/src/components/layout/rdx-page-frame.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 = ({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user