feat: add page layouts documentation and visual system design for Rdx UI components
- Introduced `page-layouts.md` to outline approved page patterns for progressive adoption. - Created `visual-system-design.md` to consolidate visual, architectural, and adoption decisions for a uniform UI/UX system in the ERP frontend. - Added `RdxEditorActionBar` component for managing action bars in editor screens. - Implemented `RdxPanel` component for grouping related content with optional headers and actions.
This commit is contained in:
parent
d92ab05f35
commit
3d2ce8b981
@ -1,14 +1,14 @@
|
||||
import {
|
||||
RdxBadge,
|
||||
RdxContextPanel,
|
||||
RdxEditorActionBar,
|
||||
RdxEditorPageLayout,
|
||||
RdxEditorSection,
|
||||
RdxEditorStickyFooter,
|
||||
RdxFormGrid,
|
||||
RdxPageActions,
|
||||
RdxPageContainer,
|
||||
RdxPageHeader,
|
||||
RdxPageStack,
|
||||
RdxPanel,
|
||||
RdxRowActions,
|
||||
RightPanel,
|
||||
} from "@repo/rdx-ui/components";
|
||||
@ -761,26 +761,26 @@ export default function UiKitPage() {
|
||||
</div>
|
||||
</ExampleCard>
|
||||
|
||||
<ExampleCard title="RdxEditorSection plain and panel">
|
||||
<ExampleCard title="RdxPanel plain and panel">
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<SurfaceExample>
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description="Default section without card."
|
||||
icon={<PlusIcon className="size-4" />}
|
||||
title="Plain section"
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">Section body</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</SurfaceExample>
|
||||
|
||||
<SurfaceExample>
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description="Independent surface reserved for exceptional blocks."
|
||||
title="Panel section"
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">Panel body</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</SurfaceExample>
|
||||
</div>
|
||||
</ExampleCard>
|
||||
@ -814,7 +814,7 @@ export default function UiKitPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description="Primary customer information"
|
||||
icon={<PlusIcon className="size-4" />}
|
||||
title="Basic information"
|
||||
@ -827,9 +827,9 @@ export default function UiKitPage() {
|
||||
Tax ID field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description="Secondary preferences"
|
||||
icon={<PlusIcon className="size-4" />}
|
||||
title="Preferences"
|
||||
@ -842,7 +842,7 @@ export default function UiKitPage() {
|
||||
Currency field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -873,16 +873,13 @@ export default function UiKitPage() {
|
||||
<RdxEditorPageLayout
|
||||
aside={
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<RdxEditorSection
|
||||
description="Secondary recipient controls."
|
||||
title="Recipient"
|
||||
>
|
||||
<RdxPanel description="Secondary recipient controls." title="Recipient">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
Customer selector and helper actions
|
||||
</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description="Fiscal defaults and toggles."
|
||||
title="Tax settings"
|
||||
>
|
||||
@ -894,7 +891,7 @@ export default function UiKitPage() {
|
||||
Default tax selector
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</div>
|
||||
}
|
||||
complexity="complex"
|
||||
@ -905,7 +902,7 @@ export default function UiKitPage() {
|
||||
Inline feedback aligned with the editor content
|
||||
</div>
|
||||
|
||||
<RdxEditorSection title="Document information">
|
||||
<RdxPanel title="Document information">
|
||||
<RdxFormGrid>
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||
Reference field
|
||||
@ -917,7 +914,7 @@ export default function UiKitPage() {
|
||||
Series field
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<section className="min-w-0 border-b border-border pb-6">
|
||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
@ -930,7 +927,7 @@ export default function UiKitPage() {
|
||||
</RdxEditorPageLayout>
|
||||
</div>
|
||||
|
||||
<RdxEditorStickyFooter
|
||||
<RdxEditorActionBar
|
||||
leading={
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-5 gap-y-2 text-sm">
|
||||
<span className="text-muted-foreground">Sticky totals region</span>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import type React from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
import { AppMain } from "./app-main";
|
||||
|
||||
@ -8,11 +8,14 @@ interface AppFullscreenLayoutFrameProps {
|
||||
}
|
||||
|
||||
export const AppFullscreenLayoutFrame = ({ children }: AppFullscreenLayoutFrameProps) => {
|
||||
return (
|
||||
<AppMain className="h-dvh w-dvw">{children}</AppMain>
|
||||
);
|
||||
return <AppMain className="h-dvh w-dvw">{children}</AppMain>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shell de aplicación para páginas que ocupan toda el área disponible.
|
||||
* Se utiliza en editores y vistas de detalle sin sidebar ni topbar global.
|
||||
*/
|
||||
|
||||
export const AppFullscreenLayout = () => {
|
||||
return (
|
||||
<AppFullscreenLayoutFrame>
|
||||
|
||||
@ -6,6 +6,19 @@ interface AppMainProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Área principal de contenido de la aplicación.
|
||||
*
|
||||
* Ocupa todo el espacio disponible dentro de su contenedor padre.
|
||||
* No decide cómo se muestra su contenido.
|
||||
*/
|
||||
|
||||
export const AppMain = ({ children, className }: AppMainProps) => {
|
||||
return <main className={cn("min-h-0 flex-1 overflow-auto bg-muted", className)}>{children}</main>;
|
||||
return (
|
||||
<main
|
||||
className={cn("flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-muted", className)}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ export const AppSidebarLayoutFrame = ({ children }: AppSidebarLayoutFrameProps)
|
||||
}
|
||||
>
|
||||
<AppSidebar />
|
||||
<SidebarInset className="flex h-screen min-w-0 flex-col overflow-hidden bg-background">
|
||||
<SidebarInset className="flex h-dvh min-w-0 flex-col overflow-hidden bg-background">
|
||||
<AppTopbar />
|
||||
<AppMain>{children}</AppMain>
|
||||
</SidebarInset>
|
||||
@ -29,6 +29,17 @@ export const AppSidebarLayoutFrame = ({ children }: AppSidebarLayoutFrameProps)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shell principal de la aplicación con navegación lateral.
|
||||
*
|
||||
* Compone:
|
||||
* - sidebar global
|
||||
* - topbar global
|
||||
* - área principal de contenido
|
||||
*
|
||||
* No decide cómo se muestra su contenido.
|
||||
*/
|
||||
|
||||
export const AppSidebarLayout = () => {
|
||||
return (
|
||||
<AppSidebarLayoutFrame>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import type React from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
interface AuthLayoutFrameProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@ -66,7 +66,7 @@ Deben recibir props ya preparadas por la capa consumidora.
|
||||
|
||||
- por defecto, las pantallas cuya finalidad principal sea crear, editar o configurar mediante formulario deben usar shell `app-fullscreen` y no el shell con sidebar;
|
||||
- usar `PageFormHeader` o el header de dominio ya existente para la navegación mínima de salida, sin duplicar una segunda navegación lateral;
|
||||
- `RdxEditorSection` con `variant="plain"` es el patrón por defecto del editor simple;
|
||||
- `RdxPanel` con `variant="plain"` es el patrón por defecto del editor simple;
|
||||
- usar una única superficie editable cuando sea útil separar claramente la página del área de formulario;
|
||||
- no envolver cada sección en `Card` por defecto;
|
||||
- no duplicar padding entre shell, layout y superficie editable;
|
||||
@ -81,7 +81,7 @@ Deben recibir props ya preparadas por la capa consumidora.
|
||||
- el `aside` debe apilarse debajo del `main` cuando no haya ancho suficiente;
|
||||
- si no existe `aside`, no debe reservarse hueco lateral;
|
||||
- mantener una única superficie principal para el formulario, también en editores complejos;
|
||||
- `RdxEditorSection` puede usar `variant="panel"` dentro del `aside` cuando el bloque necesite superficie independiente;
|
||||
- `RdxPanel` puede usar `variant="card"` dentro del `aside` cuando el bloque necesite superficie independiente;
|
||||
- `RdxFormGrid` debe quedarse en grupos de campos y no envolver tablas, line editors ni layouts funcionales especializados;
|
||||
- no mover los totales al `aside` por defecto;
|
||||
- si el editor usa footer sticky inferior, no duplicar `Guardar` y `Cancelar` completos en el header;
|
||||
@ -94,8 +94,8 @@ Reglas:
|
||||
scroll container
|
||||
└── RdxEditorPageLayout complexity="simple"
|
||||
└── form stack
|
||||
├── RdxEditorSection
|
||||
├── RdxEditorSection
|
||||
├── RdxPanel
|
||||
├── RdxPanel
|
||||
└── ...
|
||||
```
|
||||
|
||||
@ -105,7 +105,7 @@ Reglas:
|
||||
- listados, dashboards, detalle y navegación de settings mantienen `app-sidebar` salvo necesidad funcional explícita;
|
||||
- `RdxEditorPageLayout` resuelve padding interior, ancho centrado y `min-w-0`;
|
||||
- el editor simple puede apoyarse en una única superficie contenedora para diferenciar la página del área editable, sin volver a cards por sección;
|
||||
- `RdxEditorSection` expresa jerarquía por defecto con spacing y divisor inferior;
|
||||
- `RdxPanel` expresa jerarquía por defecto con spacing y divisor inferior;
|
||||
- `RdxFormGrid` mantiene la retícula del formulario sin conocer RHF ni dominio;
|
||||
- priorizar lectura y velocidad de escaneo;
|
||||
- usar secciones planas con espacio o divisores;
|
||||
@ -122,9 +122,9 @@ Ejemplo validado en `customers/update`, `customers/create` y `/account`:
|
||||
{notice}
|
||||
|
||||
<form className="flex min-w-0 flex-col gap-6">
|
||||
<RdxEditorSection title={title} description={description}>
|
||||
<RdxPanel title={title} description={description}>
|
||||
<RdxFormGrid>{fields}</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</form>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
@ -166,8 +166,8 @@ shell
|
||||
│ └── RdxEditorPageLayout complexity="complex"
|
||||
│ ├── main
|
||||
│ │ └── única superficie editable
|
||||
│ │ ├── RdxEditorSection
|
||||
│ │ ├── RdxEditorSection
|
||||
│ │ ├── RdxPanel
|
||||
│ │ ├── RdxPanel
|
||||
│ │ ├── bloque de líneas con layout propio
|
||||
│ │ └── ...
|
||||
│ └── aside
|
||||
@ -206,9 +206,9 @@ Ejemplo validado en `proformas/update`:
|
||||
>
|
||||
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<form className="flex min-w-0 flex-col gap-6">
|
||||
<RdxEditorSection title={documentTitle}>
|
||||
<RdxPanel title={documentTitle}>
|
||||
<RdxFormGrid>{documentFields}</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
{lineEditor}
|
||||
</form>
|
||||
@ -28,7 +28,7 @@ RdxContextPanel
|
||||
RdxRowActions
|
||||
RdxListPageLayout
|
||||
RdxEditorPageLayout
|
||||
RdxEditorSection
|
||||
RdxPanel
|
||||
RdxFormGrid
|
||||
RdxEditorStickyFooter
|
||||
RdxDetailPageLayout
|
||||
@ -525,7 +525,7 @@ RdxPageActions
|
||||
RdxContextPanel
|
||||
RdxRowActions
|
||||
RdxEditorPageLayout
|
||||
RdxEditorSection
|
||||
RdxPanel
|
||||
RdxFormGrid
|
||||
RdxEditorStickyFooter
|
||||
```
|
||||
@ -1074,14 +1074,14 @@ Usar `customers/update` como piloto para:
|
||||
|
||||
```txt
|
||||
RdxEditorPageLayout
|
||||
RdxEditorSection
|
||||
RdxPanel
|
||||
RdxFormGrid
|
||||
```
|
||||
|
||||
Resultado observado a 6 de agosto de 2026:
|
||||
|
||||
- `RdxEditorPageLayout` absorbe el ancho centrado y el padding interior del editor simple;
|
||||
- `RdxEditorSection` sustituye en `customers/update` el uso sistemático de `FormSectionCard` cuando la superficie no necesita card;
|
||||
- `RdxPanel` sustituye en `customers/update` el uso sistemático de `FormSectionCard` cuando la superficie no necesita card;
|
||||
- `RdxFormGrid` consolida la retícula ya existente sin mover lógica de formulario fuera del consumidor;
|
||||
- `PageFormHeader` se mantiene sin cambios funcionales;
|
||||
- queda pendiente validar visualmente si el header sticky full-width se percibe demasiado separado del ancho centrado del editor.
|
||||
@ -1096,7 +1096,7 @@ customers/create
|
||||
|
||||
Resultado observado a 6 de agosto de 2026:
|
||||
|
||||
- `customers/create` reutiliza `RdxEditorPageLayout`, `RdxEditorSection` y `RdxFormGrid` sin requerir nuevas primitives;
|
||||
- `customers/create` reutiliza `RdxEditorPageLayout`, `RdxPanel` y `RdxFormGrid` sin requerir nuevas primitives;
|
||||
- create y update comparten shell, header legacy, superficie editable única y secciones planas internas;
|
||||
- las diferencias reales quedan en defaults, submit y wiring del controller, no en la composición visual;
|
||||
- `PageFormHeader` sigue siendo funcionalmente suficiente, aunque se mantiene una deuda visual menor por su anchura full-width frente al contenido centrado.
|
||||
@ -1120,7 +1120,7 @@ Resultado observado a 10 de agosto de 2026:
|
||||
- el footer sticky de totales se mantiene como dominio visual y económico de Proformas;
|
||||
- `RdxEditorStickyFooter` queda justificado para combinar en una única superficie sticky la región de totales/contexto y las acciones `Cancelar` / `Guardar`;
|
||||
- `PageFormHeader` queda reducido a navegación y contexto solo en `proformas/update`, sin cambio global del componente legacy;
|
||||
- `RdxEditorSection` y `RdxFormGrid` siguen siendo válidos para bloques de campos, pero no deben forzar el layout del editor de líneas ni el footer de totales.
|
||||
- `RdxPanel` y `RdxFormGrid` siguen siendo válidos para bloques de campos, pero no deben forzar el layout del editor de líneas ni el footer de totales.
|
||||
|
||||
## Fase 8 — Detalle
|
||||
|
||||
@ -1166,9 +1166,9 @@ No hacer:
|
||||
- `RightPanel` es suficiente como primitive de superficie lateral y no debe renombrarse globalmente.
|
||||
- `RdxContextPanel` está justificado como composición visual común sobre `RightPanel`.
|
||||
- `RdxRowActions` está justificado para normalizar icon buttons, overflow, disabled reason y destructivas en tablas.
|
||||
- `RdxEditorPageLayout`, `RdxEditorSection` y `RdxFormGrid` quedan confirmados tras migrar `customers/update` y `customers/create`.
|
||||
- `RdxEditorPageLayout`, `RdxPanel` y `RdxFormGrid` quedan confirmados tras migrar `customers/update` y `customers/create`.
|
||||
- `RdxEditorPageLayout` también es apto para `proformas/update` cuando el `aside` contiene bloques secundarios ya existentes y el `main` conserva la única superficie editable.
|
||||
- `RdxEditorSection` soporta tanto secciones planas de formulario como grupos secundarios del `aside` sin volver a `Card` por defecto.
|
||||
- `RdxPanel` soporta tanto secciones planas de formulario como grupos secundarios del `aside` sin volver a `Card` por defecto.
|
||||
- `RdxFormGrid` queda confirmado para campos en editores complejos, pero no debe envolver tablas o editores de líneas.
|
||||
- `RdxEditorStickyFooter` está justificado para el patrón de editor complejo con una sola zona inferior persistente y sin duplicidad de acciones con el header.
|
||||
|
||||
@ -119,7 +119,7 @@ Todos los textos visibles pertenecen al namespace `Suppliers`. Se mantienen recu
|
||||
## 17. Componentes reutilizados
|
||||
|
||||
- `RdxPageContainer`, `RdxPageStack`, `RdxPageHeader`, `RdxPageActions`;
|
||||
- `RdxEditorPageLayout`, `RdxEditorSection`, `RdxFormGrid`;
|
||||
- `RdxEditorPageLayout`, `RdxPanel`, `RdxFormGrid`;
|
||||
- `DataTable`, `SkeletonDataTable`, `DataTableColumnHeader`, `RdxRowActions`;
|
||||
- `TextField`, `SelectField`, `PageFormHeader`, `FormActionsBar`;
|
||||
- `SimpleSearchInput`, `PageErrorState`, `ErrorAlert`;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mock, test } from "node:test";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
@ -73,13 +74,9 @@ mock.module("@repo/rdx-ui/components", {
|
||||
capturedResetProps = props;
|
||||
return null;
|
||||
},
|
||||
RdxEditorSection: ({
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
}) => <section data-title={title}>{children}</section>,
|
||||
RdxPanel: ({ children, title }: { children: ReactNode; title: string }) => (
|
||||
<section data-title={title}>{children}</section>
|
||||
),
|
||||
RdxEditorStickyFooter: () => null,
|
||||
RdxFormGrid: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
RhfSubmitActionButton: (props: Record<string, unknown>) => {
|
||||
@ -108,7 +105,7 @@ mock.module("../../../../i18n", {
|
||||
useTranslation: () => ({
|
||||
t: (key: string) =>
|
||||
(
|
||||
{
|
||||
({
|
||||
"settings.companyDetails.title": "Datos de empresa",
|
||||
"settings.companyDetails.loading": "Cargando datos de la empresa activa...",
|
||||
"settings.companyDetails.description":
|
||||
@ -157,7 +154,8 @@ mock.module("../../../../i18n", {
|
||||
"Debe pertenecer a la empresa activa y estar activo para nuevas asignaciones.",
|
||||
"settings.companyDetails.sections.paymentTerm.field.placeholder":
|
||||
"Selecciona un vencimiento",
|
||||
"settings.companyDetails.sections.paymentTerm.field.none": "Sin vencimiento por defecto",
|
||||
"settings.companyDetails.sections.paymentTerm.field.none":
|
||||
"Sin vencimiento por defecto",
|
||||
"settings.companyDetails.actions.save": "Guardar cambios",
|
||||
"settings.companyDetails.actions.saving": "Guardando...",
|
||||
"settings.companyDetails.actions.reset": "Restablecer",
|
||||
@ -165,7 +163,7 @@ mock.module("../../../../i18n", {
|
||||
"settings.companyDetails.feedback.saving": "Guardando configuracion de empresa.",
|
||||
"settings.companyDetails.pageError.actions.retry": "Reintentar",
|
||||
"settings.companyDetails.pageError.actions.retrying": "Reintentando...",
|
||||
} as Record<string, string>
|
||||
}) as Record<string, string>
|
||||
)[key] ?? key,
|
||||
}),
|
||||
},
|
||||
|
||||
@ -3,9 +3,9 @@ import { UnsavedChangesProvider } from "@erp/core/hooks";
|
||||
import {
|
||||
AppContent,
|
||||
CancelActionButton,
|
||||
RdxEditorSection,
|
||||
RdxEditorStickyFooter,
|
||||
RdxEditorActionBar,
|
||||
RdxFormGrid,
|
||||
RdxPanel,
|
||||
RhfSubmitActionButton,
|
||||
SelectField,
|
||||
TextField,
|
||||
@ -88,11 +88,11 @@ export const CompanyDetailsSettingsPage = () => {
|
||||
onKeyDown={ctrl.onFormKeyDown}
|
||||
onSubmit={ctrl.onSubmit}
|
||||
>
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("settings.companyDetails.sections.general.description")}
|
||||
icon={<ScrollTextIcon className="size-5" />}
|
||||
title={t("settings.companyDetails.sections.general.title")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<TextField<CompanySettingsForm>
|
||||
@ -132,13 +132,13 @@ export const CompanyDetailsSettingsPage = () => {
|
||||
placeholder={t("settings.companyDetails.sections.general.fields.tin.placeholder")}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("settings.companyDetails.sections.contact.description")}
|
||||
icon={<ContactIcon className="size-5" />}
|
||||
title={t("settings.companyDetails.sections.contact.title")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<TextField<CompanySettingsForm>
|
||||
@ -184,13 +184,13 @@ export const CompanyDetailsSettingsPage = () => {
|
||||
typePreset="url"
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("settings.companyDetails.sections.paymentTerm.description")}
|
||||
icon={<LandmarkIcon className="size-5" />}
|
||||
title={t("settings.companyDetails.sections.paymentTerm.title")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<SelectField<CompanySettingsForm>
|
||||
@ -207,7 +207,7 @@ export const CompanyDetailsSettingsPage = () => {
|
||||
placeholder={t("settings.companyDetails.sections.paymentTerm.field.placeholder")}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</form>
|
||||
|
||||
<p aria-live="polite" className="sr-only">
|
||||
@ -216,7 +216,7 @@ export const CompanyDetailsSettingsPage = () => {
|
||||
: t("settings.companyDetails.feedback.idle")}
|
||||
</p>
|
||||
|
||||
<RdxEditorStickyFooter
|
||||
<RdxEditorActionBar
|
||||
primary={
|
||||
<RhfSubmitActionButton
|
||||
busyLabel={t("settings.companyDetails.actions.saving")}
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ProformaDetailActionsViewModel,
|
||||
ProformaDetailHeaderViewModel,
|
||||
} from "../../entities";
|
||||
import { ProformaDetailHeaderMeta } from "../components";
|
||||
import { ProformaDetailArchivedBadge, ProformaDetailHeaderMeta } from "../components";
|
||||
|
||||
import { ProformaDetailHeaderActions } from "./proforma-detail-header-actions";
|
||||
|
||||
|
||||
@ -75,7 +75,9 @@ export const ProformaCompactTotals = ({
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 text-sm",
|
||||
isEmbedded ? "border-b border-border pb-4 lg:grid-cols-3 xl:gap-8" : "max-w-3xl lg:grid-cols-3 xl:max-w-5xl xl:gap-8 2xl:gap-16"
|
||||
isEmbedded
|
||||
? "border-b border-border pb-4 lg:grid-cols-3 xl:gap-8"
|
||||
: "max-w-3xl lg:grid-cols-3 xl:max-w-5xl xl:gap-8 2xl:gap-16"
|
||||
)}
|
||||
>
|
||||
{/* Descuentos */}
|
||||
@ -184,15 +186,22 @@ export const ProformaCompactTotals = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-4",
|
||||
isEmbedded ? "min-w-0 justify-between bg-transparent py-0" : "rounded-none bg-muted px-6 py-3"
|
||||
isEmbedded
|
||||
? "min-w-0 justify-between bg-transparent py-0"
|
||||
: "rounded-none bg-muted px-6 py-3"
|
||||
)}
|
||||
>
|
||||
<Button onClick={() => setIsExpanded(!isExpanded)} size="sm" type="button" variant="outline">
|
||||
<Button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{isExpanded ? <ChevronDown className="size-5" /> : <ChevronUp className="size-5" />}
|
||||
{isExpanded ? "Ocultar desglose" : "Ver desglose"}
|
||||
</Button>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-x-5 gap-y-2 text-sm">
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-x-5 gap-y-2 text-base">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<span className="text-muted-foreground">
|
||||
{t("proformas.update.totals.subtotalBeforeDiscounts", "Subtotal")}
|
||||
@ -210,7 +219,7 @@ export const ProformaCompactTotals = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<span className="sr-only">
|
||||
<span className="text-teal-700 sr-only 2xl:not-sr-only">
|
||||
{t("proformas.update.totals.totalDiscountAmount", "Dtos")}
|
||||
</span>
|
||||
<span className="font-medium tabular-nums text-teal-700 dark:text-teal-400">{`-${formatMoney(totals.totalDiscountAmount)}`}</span>
|
||||
@ -230,7 +239,7 @@ export const ProformaCompactTotals = ({
|
||||
{t("proformas.update.totals.taxTotal", "Total impuestos")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="text-muted-foreground sr-only">
|
||||
<span className=" text-orange-700 sr-only 2xl:not-sr-only">
|
||||
{t("proformas.update.totals.taxTotal", "Impuestos")}
|
||||
</span>
|
||||
<span className="font-medium tabular-nums text-orange-700 dark:text-orange-400">
|
||||
@ -246,7 +255,7 @@ export const ProformaCompactTotals = ({
|
||||
{t("proformas.update.totals.retentionAmount", "Total retenciones")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="sr-only">
|
||||
<span className="sr-only 2xl:not-sr-only">
|
||||
{t("proformas.update.totals.retentionAmount", "Retenc.")}
|
||||
</span>
|
||||
<span className="font-medium tabular-nums">
|
||||
@ -258,7 +267,7 @@ export const ProformaCompactTotals = ({
|
||||
{isEmbedded ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2">
|
||||
<ReceiptTextIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-muted-foreground">Total</span>
|
||||
<span className="text-base font-medium text-foreground">Total</span>
|
||||
<span className="text-base font-bold tabular-nums text-foreground">
|
||||
{formatMoney(totals.total)}
|
||||
</span>
|
||||
|
||||
@ -308,7 +308,9 @@ export const ProformaLineFooterEditor = ({
|
||||
<div className="flex items-end justify-between mt-4 pt-4 border-t border-gray-200">
|
||||
{/* Global discount */}
|
||||
<div className="flex items-center gap-3">
|
||||
<FormFieldLabel>Descuento global (%):</FormFieldLabel>
|
||||
<FormFieldLabel className="text-base">
|
||||
{t("proformas.update.totals.globalDiscountPercentage", "Descuento global")} (%):
|
||||
</FormFieldLabel>
|
||||
<div className="flex items-center gap-2 w-24">
|
||||
<PercentageField
|
||||
disabled={disabled}
|
||||
@ -319,7 +321,7 @@ export const ProformaLineFooterEditor = ({
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-3">
|
||||
<div className="flex items-baseline gap-4">
|
||||
<div className="flex items-baseline gap-4 font-medium">
|
||||
{t("form_fields.items.total.label", "Total dto. global")}:
|
||||
<span className=" font-medium text-amber-600 tabular-nums">
|
||||
{MoneyHelper.formatCurrency(totals.globalDiscountAmount, 2, currency)}
|
||||
@ -335,7 +337,7 @@ export const ProformaLineFooterEditor = ({
|
||||
|
||||
{/* Totals summary */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="px-6 py-3">
|
||||
<div className="px-6 py-3 font-medium">
|
||||
<div className="flex items-baseline gap-4">
|
||||
{t("form_fields.items.total.label", "Total dto. líneas")}:
|
||||
<span className=" font-medium text-amber-600 tabular-nums">
|
||||
@ -345,8 +347,8 @@ export const ProformaLineFooterEditor = ({
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border px-6 py-3">
|
||||
<div className="flex items-baseline gap-4">
|
||||
{t("form_fields.items.total.label", "Total")}:
|
||||
<div className="flex items-baseline gap-4 font-medium">
|
||||
{t("form_fields.items.total.label", "Total de líneas")}:
|
||||
<span className=" font-bold tabular-nums">
|
||||
{MoneyHelper.formatCurrency(itemsTotals.total, 2, currency)}
|
||||
</span>
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
SelectField,
|
||||
type SelectFieldItem,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { RdxFormGrid, RdxPanel, SelectField, type SelectFieldItem } from "@repo/rdx-ui/components";
|
||||
import { CreditCardIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -30,12 +25,12 @@ export const ProformaUpdatePaymentEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
className={className}
|
||||
description={t("form_groups.proformas.payment.description", "Forma de pago")}
|
||||
icon={<CreditCardIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.payment.title", "Condiciones de pago")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid className="w-full">
|
||||
<SelectField
|
||||
@ -68,6 +63,6 @@ export const ProformaUpdatePaymentEditor = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import {
|
||||
DatePickerField,
|
||||
RdxBadge,
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
RdxPanel,
|
||||
SelectField,
|
||||
type SelectFieldItem,
|
||||
TextAreaField,
|
||||
@ -29,11 +29,11 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
className={className}
|
||||
icon={<FileTextIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.basic_info.title")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-6 2xl:grid-cols-12">
|
||||
<TextField
|
||||
@ -109,6 +109,6 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RdxEditorSection } from "@repo/rdx-ui/components";
|
||||
import { RdxPanel } from "@repo/rdx-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ListIcon } from "lucide-react";
|
||||
|
||||
@ -29,7 +29,7 @@ export const ProformaUpdateItemsEditor = ({
|
||||
const { itemErrors } = itemsCtrl;
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
className={cn(
|
||||
itemErrors.length
|
||||
? "rounded-xl border border-destructive/40 bg-destructive/5 px-4 py-4 md:px-5"
|
||||
@ -37,7 +37,7 @@ export const ProformaUpdateItemsEditor = ({
|
||||
)}
|
||||
icon={<ListIcon className="size-3" />}
|
||||
title={t("form_groups.items.title", "Líneas de detalle")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<ProformaLineEditor
|
||||
addItemAtStart={itemsCtrl.addItemAtStart}
|
||||
@ -57,6 +57,6 @@ export const ProformaUpdateItemsEditor = ({
|
||||
showLineTaxes={taxCtrl.usesPerLineTax}
|
||||
totals={totals}
|
||||
/>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { RdxEditorSection } from "@repo/rdx-ui/components";
|
||||
import { RdxPanel } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon, RefreshCwIcon, UserIcon } from "lucide-react";
|
||||
|
||||
@ -32,12 +32,15 @@ export const ProformaUpdateRecipientEditor = ({
|
||||
const showActions = !readOnly;
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
className={className}
|
||||
description={t("form_groups.proformas.customer.description", "Cliente destinatario de la proforma")}
|
||||
description={t(
|
||||
"form_groups.proformas.customer.description",
|
||||
"Cliente destinatario de la proforma"
|
||||
)}
|
||||
icon={<UserIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.customer.title", "Cliente")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<div className="min-w-0 space-y-5">
|
||||
<SelectedRecipientSummary disabled={disabled} recipient={selectedCustomer} />
|
||||
@ -69,6 +72,6 @@ export const ProformaUpdateRecipientEditor = ({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
RdxPanel,
|
||||
SelectField,
|
||||
type SelectFieldItem,
|
||||
SwitchField,
|
||||
@ -38,7 +38,7 @@ export const ProformaUpdateTaxEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
className={className}
|
||||
description={t(
|
||||
"form_groups.proformas.taxes.description",
|
||||
@ -46,7 +46,7 @@ export const ProformaUpdateTaxEditor = ({
|
||||
)}
|
||||
icon={<LandmarkIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.taxes.title", "Configuración fiscal")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid>
|
||||
<SelectField
|
||||
@ -160,6 +160,6 @@ export const ProformaUpdateTaxEditor = ({
|
||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
CancelActionButton,
|
||||
PercentageField,
|
||||
RdxBadge,
|
||||
RdxEditorStickyFooter,
|
||||
RdxEditorActionBar,
|
||||
RdxPageContainer,
|
||||
RdxPageStack,
|
||||
RhfSubmitActionButton,
|
||||
@ -204,7 +204,7 @@ export const ProformaUpdatePage = () => {
|
||||
/>
|
||||
</RdxPageContainer>
|
||||
|
||||
<RdxEditorStickyFooter
|
||||
<RdxEditorActionBar
|
||||
leading={
|
||||
<ProformaCompactTotals
|
||||
currency={updateCtrl.currencyCode}
|
||||
|
||||
@ -3,14 +3,13 @@ import { UnsavedChangesProvider } from "@erp/core/hooks";
|
||||
import {
|
||||
AppContent,
|
||||
CancelActionButton,
|
||||
RdxEditorStickyFooter,
|
||||
RdxEditorActionBar,
|
||||
RhfSubmitActionButton,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { useCompanyVerifactuSettingsPageController } from "../../controllers";
|
||||
|
||||
import { CompanyVerifactuSettingsSkeleton } from "../components/company-verifactu-settings-skeleton";
|
||||
import { CompanyVerifactuConfigurationSection } from "../sections/company-verifactu-configuration-section";
|
||||
import { CompanyVerifactuDefaultsSection } from "../sections/company-verifactu-defaults-section";
|
||||
@ -85,7 +84,7 @@ export const CompanyVerifactuSettingsPage = () => {
|
||||
>
|
||||
<CompanyVerifactuStatusSection />
|
||||
|
||||
{(profileCtrl.showExemptionFields || profileCtrl.showVerifactuFields) ? (
|
||||
{profileCtrl.showExemptionFields || profileCtrl.showVerifactuFields ? (
|
||||
<CompanyVerifactuConfigurationSection
|
||||
disabled={profileCtrl.isUpdating}
|
||||
showExemptionFields={profileCtrl.showExemptionFields}
|
||||
@ -106,7 +105,7 @@ export const CompanyVerifactuSettingsPage = () => {
|
||||
: t("settings.verifactu.feedback.idle")}
|
||||
</p>
|
||||
|
||||
<RdxEditorStickyFooter
|
||||
<RdxEditorActionBar
|
||||
primary={
|
||||
<RhfSubmitActionButton
|
||||
busyLabel={t("settings.verifactu.actions.saving")}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { SelectFieldItem } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
DatePickerField,
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
RdxPanel,
|
||||
SelectField,
|
||||
TextAreaField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
@ -28,11 +28,11 @@ export const CompanyVerifactuConfigurationSection = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("settings.verifactu.configuration.description")}
|
||||
icon={<ShieldCheckIcon className="size-5" />}
|
||||
title={t("settings.verifactu.configuration.title")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
{showExemptionFields ? (
|
||||
@ -63,8 +63,8 @@ export const CompanyVerifactuConfigurationSection = ({
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
clearable
|
||||
className="col-span-full md:col-span-1"
|
||||
clearable
|
||||
description={t("settings.verifactu.auto_renew.description")}
|
||||
disabled={disabled}
|
||||
emptyValue=""
|
||||
@ -76,6 +76,6 @@ export const CompanyVerifactuConfigurationSection = ({
|
||||
</>
|
||||
) : null}
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
SelectField,
|
||||
type SelectFieldItem,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { RdxFormGrid, RdxPanel, SelectField, type SelectFieldItem } from "@repo/rdx-ui/components";
|
||||
import { LandmarkIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -22,16 +17,16 @@ export const CompanyVerifactuDefaultsSection = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("settings.verifactu.defaults.description")}
|
||||
icon={<LandmarkIcon className="size-5" />}
|
||||
title={t("settings.verifactu.defaults.title")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<SelectField
|
||||
clearable
|
||||
className="col-span-full"
|
||||
clearable
|
||||
description={t("settings.verifactu.defaults.tax_regime.description")}
|
||||
disabled={disabled || isTaxRegimesLoading}
|
||||
emptyValue=""
|
||||
@ -45,6 +40,6 @@ export const CompanyVerifactuDefaultsSection = ({
|
||||
{t("settings.verifactu.defaults.pending_catalogs")}
|
||||
</div>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FormFieldLabel, RdxEditorSection } from "@repo/rdx-ui/components";
|
||||
import { FormFieldLabel, RdxPanel } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@ -19,11 +19,11 @@ export const CompanyVerifactuStatusSection = () => {
|
||||
const { control } = useFormContext<CompanyVerifactuProfileForm>();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("settings.verifactu.status.description")}
|
||||
icon={<FileCogIcon className="size-5" />}
|
||||
title={t("settings.verifactu.status.title")}
|
||||
variant="panel"
|
||||
variant="card"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
@ -97,6 +97,6 @@ export const CompanyVerifactuStatusSection = () => {
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
FormFieldLabel,
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { FormFieldLabel, RdxFormGrid, RdxPanel, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@ -30,7 +25,7 @@ export const QuickCustomerCreateFormFields = ({
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form_groups.basic_info.description")}
|
||||
title={t("form_groups.basic_info.title")}
|
||||
>
|
||||
@ -111,9 +106,9 @@ export const QuickCustomerCreateFormFields = ({
|
||||
)}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form_groups.contact_info.description")}
|
||||
title={t("form_groups.contact_info.title")}
|
||||
>
|
||||
@ -129,7 +124,7 @@ export const QuickCustomerCreateFormFields = ({
|
||||
typePreset="email"
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RdxEditorSection, RdxFormGrid, SelectField } from "@repo/rdx-ui/components";
|
||||
import { RdxFormGrid, RdxPanel, SelectField } from "@repo/rdx-ui/components";
|
||||
import { Settings2Icon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
@ -16,7 +16,7 @@ export const CustomerAdditionalConfigEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form_groups.preferences.description")}
|
||||
icon={<Settings2Icon className="size-5" />}
|
||||
title={t("form_groups.preferences.title")}
|
||||
@ -45,6 +45,6 @@ export const CustomerAdditionalConfigEditor = ({
|
||||
required
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
SelectField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { RdxFormGrid, RdxPanel, SelectField, TextField } from "@repo/rdx-ui/components";
|
||||
import { MapPinIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
@ -21,7 +16,7 @@ export const CustomerAddressEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form_groups.address.description")}
|
||||
icon={<MapPinIcon className="size-5" />}
|
||||
title={t("form_groups.address.title")}
|
||||
@ -82,6 +77,6 @@ export const CustomerAddressEditor = ({
|
||||
required
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
FormFieldLabel,
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
RdxPanel,
|
||||
TextAreaField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
@ -32,7 +32,7 @@ export const CustomerBasicInfoEditor = ({
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form_groups.basic_info.description")}
|
||||
icon={<FileTextIcon className="size-5" />}
|
||||
title={t("form_groups.basic_info.title")}
|
||||
@ -145,6 +145,6 @@ export const CustomerBasicInfoEditor = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RdxEditorSection, RdxFormGrid, TextField } from "@repo/rdx-ui/components";
|
||||
import { RdxFormGrid, RdxPanel, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@ -23,7 +23,7 @@ export const CustomerContactEditor = ({
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form_groups.contact_info.description")}
|
||||
icon={<PhoneIcon className="size-5" />}
|
||||
title={t("form_groups.contact_info.title")}
|
||||
@ -140,6 +140,6 @@ export const CustomerContactEditor = ({
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
RdxPanel,
|
||||
SelectField,
|
||||
type SelectFieldItem,
|
||||
SwitchField,
|
||||
@ -27,7 +27,7 @@ export const CustomerFiscalEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form_groups.fiscal_info.description")}
|
||||
icon={<LandmarkIcon className="size-5" />}
|
||||
title={t("form_groups.fiscal_info.title")}
|
||||
@ -88,6 +88,6 @@ export const CustomerFiscalEditor = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,9 +2,9 @@ import { ErrorAlert, PageErrorState, PageFormHeader } from "@erp/core/components
|
||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||
import {
|
||||
CancelActionButton,
|
||||
RdxEditorActionBar,
|
||||
RdxEditorPageLayout,
|
||||
RdxEditorSection,
|
||||
RdxEditorStickyFooter,
|
||||
RdxPanel,
|
||||
RhfSubmitActionButton,
|
||||
SelectField,
|
||||
TextField,
|
||||
@ -283,7 +283,7 @@ export const AccountPage = () => {
|
||||
onKeyDown={ctrl.onFormKeyDown}
|
||||
onSubmit={ctrl.onProfileSubmit}
|
||||
>
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("account.sections.personal.description")}
|
||||
title={t("account.sections.personal.title")}
|
||||
>
|
||||
@ -337,7 +337,7 @@ export const AccountPage = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</form>
|
||||
|
||||
<p aria-live="polite" className="sr-only">
|
||||
@ -346,7 +346,7 @@ export const AccountPage = () => {
|
||||
: t("account.feedback.idle")}
|
||||
</p>
|
||||
|
||||
<RdxEditorStickyFooter
|
||||
<RdxEditorActionBar
|
||||
primary={
|
||||
<RhfSubmitActionButton
|
||||
busyLabel={t("account.actions.savingProfile")}
|
||||
@ -369,7 +369,7 @@ export const AccountPage = () => {
|
||||
</UnsavedChangesProvider>
|
||||
</FormProvider>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("account.sections.security.description")}
|
||||
title={t("account.sections.security.title")}
|
||||
>
|
||||
@ -387,9 +387,9 @@ export const AccountPage = () => {
|
||||
{t("account.actions.changePassword")}
|
||||
</Button>
|
||||
</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("account.sections.session.description")}
|
||||
title={t("account.sections.session.title")}
|
||||
>
|
||||
@ -401,7 +401,7 @@ export const AccountPage = () => {
|
||||
{t("account.actions.logout")}
|
||||
</Button>
|
||||
</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</div>
|
||||
</div>
|
||||
</RdxEditorPageLayout>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
FormFieldLabel,
|
||||
RdxEditorSection,
|
||||
RdxFormGrid,
|
||||
RdxPanel,
|
||||
SelectField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
@ -58,7 +58,7 @@ export const SupplierEditorForm = ({
|
||||
|
||||
return (
|
||||
<form className="flex min-w-0 flex-col gap-6" id={formId} noValidate onSubmit={onSubmit}>
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form.sections.identity.description")}
|
||||
icon={<FileTextIcon className="size-5" />}
|
||||
title={t("form.sections.identity.title")}
|
||||
@ -105,9 +105,9 @@ export const SupplierEditorForm = ({
|
||||
<TextField disabled={isSubmitting} label={t("fields.trade_name")} name="tradeName" />
|
||||
<TextField disabled={isSubmitting} label={t("fields.reference")} name="reference" />
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form.sections.address.description")}
|
||||
icon={<MapPinIcon className="size-5" />}
|
||||
title={t("form.sections.address.title")}
|
||||
@ -125,9 +125,9 @@ export const SupplierEditorForm = ({
|
||||
name="country"
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form.sections.contact.description")}
|
||||
icon={<PhoneIcon className="size-5" />}
|
||||
title={t("form.sections.contact.title")}
|
||||
@ -168,9 +168,9 @@ export const SupplierEditorForm = ({
|
||||
<TextField disabled={isSubmitting} label={t("fields.fax")} name="fax" />
|
||||
<TextField disabled={isSubmitting} label={t("fields.website")} name="website" />
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
description={t("form.sections.preferences.description")}
|
||||
icon={<LanguagesIcon className="size-5" />}
|
||||
title={t("form.sections.preferences.title")}
|
||||
@ -191,7 +191,7 @@ export const SupplierEditorForm = ({
|
||||
required
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export * from "./rdx-editor-action-bar.tsx";
|
||||
export * from "./rdx-editor-page-layout.tsx";
|
||||
export * from "./rdx-editor-section.tsx";
|
||||
export * from "./rdx-editor-sticky-footer.tsx";
|
||||
export * from "./rdx-panel.tsx";
|
||||
|
||||
@ -1,29 +1,43 @@
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
|
||||
export interface RdxEditorStickyFooterProps
|
||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||
export interface RdxEditorActionBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||
leading?: ReactNode;
|
||||
status?: ReactNode;
|
||||
secondary?: ReactNode;
|
||||
primary?: ReactNode;
|
||||
sticky?: boolean;
|
||||
}
|
||||
|
||||
export const RdxEditorStickyFooter = ({
|
||||
/**
|
||||
* Barra inferior de acciones de una pantalla de edición.
|
||||
*
|
||||
* Puede mostrar:
|
||||
* - contenido auxiliar a la izquierda
|
||||
* - estado de edición
|
||||
* - acciones secundarias
|
||||
* - acción principal
|
||||
*
|
||||
* Puede funcionar dentro o fuera de un área scrollable.
|
||||
*/
|
||||
|
||||
export const RdxEditorActionBar = ({
|
||||
leading,
|
||||
status,
|
||||
secondary,
|
||||
primary,
|
||||
sticky = false,
|
||||
className,
|
||||
...props
|
||||
}: RdxEditorStickyFooterProps) => {
|
||||
}: RdxEditorActionBarProps) => {
|
||||
const hasLeading = Boolean(leading);
|
||||
const hasActionRegion = Boolean(status || secondary || primary);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"sticky bottom-0 z-20 shrink-0 border-t border-border bg-background shadow-xs",
|
||||
"shrink-0 border-t border-border bg-background shadow-xs",
|
||||
sticky && "sticky bottom-0 z-20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -40,9 +54,10 @@ export const RdxEditorStickyFooter = ({
|
||||
>
|
||||
{status ? <div className="min-w-0 text-sm text-muted-foreground">{status}</div> : null}
|
||||
|
||||
{(secondary || primary) ? (
|
||||
{secondary || primary ? (
|
||||
<div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:justify-end">
|
||||
{secondary ? <div className="flex min-w-0">{secondary}</div> : null}
|
||||
|
||||
{primary ? <div className="flex min-w-0">{primary}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
@ -5,9 +5,9 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { RdxFormGrid } from "../form/rdx-form-grid.tsx";
|
||||
|
||||
import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
|
||||
import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx";
|
||||
import { RdxEditorSection } from "./rdx-editor-section.tsx";
|
||||
import { RdxEditorStickyFooter } from "./rdx-editor-sticky-footer.tsx";
|
||||
import { RdxPanel } from "./rdx-panel.tsx";
|
||||
|
||||
test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza children", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
@ -47,16 +47,16 @@ test("RdxEditorPageLayout no reserva aside cuando no existe incluso en complex",
|
||||
assert.doesNotMatch(html, /grid-cols-\[/);
|
||||
});
|
||||
|
||||
test("RdxEditorSection plain renderiza header semántico y contenido sin card", () => {
|
||||
test("RdxPanel plain renderiza header semántico y contenido sin card", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorSection
|
||||
<RdxPanel
|
||||
actions={<button type="button">Action</button>}
|
||||
description="Section description"
|
||||
icon={<span>Icon</span>}
|
||||
title="Section title"
|
||||
>
|
||||
<div>Section body</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
|
||||
assert.match(html, /<h2[^>]*>Section title<\/h2>/);
|
||||
@ -66,11 +66,11 @@ test("RdxEditorSection plain renderiza header semántico y contenido sin card",
|
||||
assert.doesNotMatch(html, /shadow/);
|
||||
});
|
||||
|
||||
test("RdxEditorSection panel conserva superficie independiente", () => {
|
||||
test("RdxPanel panel conserva superficie independiente", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorSection title="Panel section" variant="panel">
|
||||
<RdxPanel title="Panel section" variant="card">
|
||||
<div>Panel body</div>
|
||||
</RdxEditorSection>
|
||||
</RdxPanel>
|
||||
);
|
||||
|
||||
assert.match(html, /rounded-xl/);
|
||||
@ -95,7 +95,7 @@ test("RdxFormGrid mantiene la retícula base y combina className", () => {
|
||||
|
||||
test("RdxEditorStickyFooter renderiza regiones presentes sin wrappers vacíos", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorStickyFooter
|
||||
<RdxEditorActionBar
|
||||
className="custom-sticky"
|
||||
leading={<div>Totals summary</div>}
|
||||
primary={<button type="submit">Save</button>}
|
||||
@ -116,7 +116,7 @@ test("RdxEditorStickyFooter renderiza regiones presentes sin wrappers vacíos",
|
||||
|
||||
test("RdxEditorStickyFooter omite regiones opcionales vacías", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<RdxEditorStickyFooter primary={<button type="submit">Save</button>} />
|
||||
<RdxEditorActionBar primary={<button type="submit">Save</button>} />
|
||||
);
|
||||
|
||||
assert.match(html, /Save/);
|
||||
|
||||
@ -20,6 +20,15 @@ const ASIDE_GRID_CLASSNAME: Record<RdxEditorWidth, string> = {
|
||||
full: "xl:grid-cols-[minmax(0,1fr)_22rem]",
|
||||
};
|
||||
|
||||
/**
|
||||
* Distribuye el contenido interno de un contenedor en:
|
||||
*
|
||||
* - una región principal
|
||||
* - un aside opcional.
|
||||
*
|
||||
* No controla padding, scroll ni estructura general de página.
|
||||
*/
|
||||
|
||||
export const RdxEditorPageLayout = ({
|
||||
children,
|
||||
aside,
|
||||
|
||||
@ -2,24 +2,37 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import { useId } from "react";
|
||||
|
||||
export type RdxEditorSectionVariant = "plain" | "panel";
|
||||
export type RdxPanelVariant = "plain" | "card";
|
||||
|
||||
export interface RdxEditorSectionProps
|
||||
export interface RdxPanelProps
|
||||
extends Omit<ComponentPropsWithoutRef<"section">, "children" | "title"> {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
variant?: RdxEditorSectionVariant;
|
||||
variant?: RdxPanelVariant;
|
||||
}
|
||||
|
||||
const VARIANT_CLASSNAME: Record<RdxEditorSectionVariant, string> = {
|
||||
const VARIANT_CLASSNAME: Record<RdxPanelVariant, string> = {
|
||||
plain: "min-w-0 border-b border-border pb-6 last:border-b-0 last:pb-0",
|
||||
panel: "min-w-0 rounded-xl border border-border bg-background p-4 shadow-xs sm:p-5",
|
||||
card: "min-w-0 rounded-xl border border-border bg-background p-4 shadow-xs sm:p-5",
|
||||
};
|
||||
|
||||
export const RdxEditorSection = ({
|
||||
/**
|
||||
* Contenedor para agrupar contenido relacionado.
|
||||
*
|
||||
* Proporciona:
|
||||
* - título
|
||||
* - descripción
|
||||
* - icono
|
||||
* - acciones
|
||||
* - contenido
|
||||
*
|
||||
* Puede representarse sin border (`plain`) o como una tarjeta (`card`).
|
||||
*/
|
||||
|
||||
export const RdxPanel = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
@ -28,7 +41,7 @@ export const RdxEditorSection = ({
|
||||
variant = "plain",
|
||||
className,
|
||||
...props
|
||||
}: RdxEditorSectionProps) => {
|
||||
}: RdxPanelProps) => {
|
||||
const titleId = useId();
|
||||
const hasHeader = Boolean(title || description || icon || actions);
|
||||
|
||||
@ -7,6 +7,16 @@ export interface RdxPageActionsProps extends Omit<ComponentPropsWithoutRef<"div"
|
||||
overflow?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contenedor para mostrar las acciones de una página según su importancia.
|
||||
*
|
||||
* Agrupa:
|
||||
* - acciones secundarias
|
||||
* - acción principal
|
||||
* - acciones overflow
|
||||
*
|
||||
*/
|
||||
|
||||
export const RdxPageActions = ({
|
||||
primary,
|
||||
secondary,
|
||||
@ -14,7 +24,7 @@ export const RdxPageActions = ({
|
||||
className,
|
||||
...props
|
||||
}: RdxPageActionsProps) => {
|
||||
if (!primary && !secondary && !overflow) {
|
||||
if (!(primary || secondary || overflow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,21 @@ const PADDING_CLASSNAME: Record<RdxPageContainerPadding, string> = {
|
||||
none: "",
|
||||
};
|
||||
|
||||
/**
|
||||
* Contenedor visual del contenido de una página.
|
||||
*
|
||||
* Controla:
|
||||
* - ancho máximo
|
||||
* - centrado horizontal
|
||||
* - padding exterior
|
||||
*
|
||||
* No controla:
|
||||
* - scroll
|
||||
* - altura de la página
|
||||
* - header/footer
|
||||
* - estructura del editor
|
||||
*/
|
||||
|
||||
export const RdxPageContainer = ({
|
||||
children,
|
||||
className,
|
||||
|
||||
@ -21,6 +21,20 @@ export interface RdxPageHeaderProps
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cabecera estándar de una página.
|
||||
*
|
||||
* Muestra:
|
||||
* - 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 = ({
|
||||
title,
|
||||
description,
|
||||
|
||||
@ -15,6 +15,11 @@ const SPACING_CLASSNAME: Record<RdxPageStackSpacing, string> = {
|
||||
relaxed: "gap-8",
|
||||
};
|
||||
|
||||
/**
|
||||
* Contenedor para layout para apilar contenido verticalmente.
|
||||
* No proporciona estructura de página ni controla scroll.
|
||||
*/
|
||||
|
||||
export const RdxPageStack = ({
|
||||
children,
|
||||
className,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user