Añadir secciones de detalle y mejorar la presentación de activos y recursos
This commit is contained in:
parent
5a7960302e
commit
d7eb91ee68
@ -301,3 +301,11 @@ El desplazamiento vertical se conserva cuando el contenido supera la altura disp
|
||||
La pantalla `CompanyOverview` tiene composición independiente de `DashboardPage` (Inicio). Comparte tarjetas, bloques de trabajo/avisos/actividad y consultas existentes, siempre fijando el ámbito al UUID de la compañía. El encabezado «De un vistazo» precede las acciones rápidas (ticket, intervención, activo y recurso), cuatro indicadores operativos y una franja navegable de personas, departamentos y sedes. No muestra compañías activas ni el resumen por compañías. La variante compacta de `StatisticCard` mejora la jerarquía del valor y la etiqueta sin modificar la presentación de Inicio.
|
||||
|
||||
La navegación de compañía empieza por Resumen, Tickets e Intervenciones; después agrupa Personas, Puestos de trabajo y Perfiles de usuario y finalmente las demás secciones. Los grupos tienen separación visual. Las operaciones de usuario permanecen en el menú superior y dejan de repetirse al pie del menú de compañía.
|
||||
|
||||
## Fichas de consulta y resumen lateral
|
||||
|
||||
Las secciones de consulta evitan recuadros anidados y rellenos laterales: usan títulos compactos y un separador tenue entre grupos. Las agrupaciones de características siguen este mismo criterio en lectura; los formularios conservan su organización propia.
|
||||
|
||||
`DetailSection` ofrece agrupación plegable nativa (`details/summary`), abierta inicialmente, accesible por teclado. `DetailField` devuelve una fila semántica `dt/dd` para incluir en un `dl`: etiqueta y valor se alinean horizontalmente cuando el contenedor dispone de al menos 240 px y se apilan en espacios menores. El consumidor aporta los enlaces; el componente común les aplica cursor de enlace, color semántico y subrayado al pasar el puntero. Los valores ausentes permanecen como texto.
|
||||
|
||||
La ficha lateral de activos utiliza un ancho adaptable entre 320 y 400 px, acciones debajo del título y bloques de identificación, ubicación/asignación, características, observaciones, relaciones y asistencia. Conserva las secciones capturadas de la plantilla, incluyendo la compatibilidad con especificaciones antiguas sin agrupación; no modifica valores ni instantáneas. Las relaciones llevan a destinos existentes: persona y activo relacionado a su ficha; sede, puesto y plantilla a sus listados/catálogos actuales. La vista completa e historial mantienen sus funciones. Las fichas de recursos reutilizan las filas de características. No se crean nuevas fichas laterales ni nuevas rutas.
|
||||
|
||||
@ -303,5 +303,10 @@
|
||||
"formLayout": {
|
||||
"identity": "Identification and assignment",
|
||||
"template": "Template details"
|
||||
},
|
||||
"detailSections": {
|
||||
"identification": "Identification",
|
||||
"assignment": "Location and assignment",
|
||||
"notes": "Notes"
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,5 +303,10 @@
|
||||
"formLayout": {
|
||||
"identity": "Identificación y asignación",
|
||||
"template": "Datos de la plantilla"
|
||||
},
|
||||
"detailSections": {
|
||||
"identification": "Identificación",
|
||||
"assignment": "Ubicación y asignación",
|
||||
"notes": "Observaciones"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export type AssetRelationshipsProps = {
|
||||
compact?: boolean;
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
assetId: string;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { BrowserLink } from "@metis/core/client";
|
||||
import { Button } from "@repo/shadcn-ui/components/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -8,7 +9,7 @@ import { useAssetRelationshipsController } from "../controllers";
|
||||
import type { AssetRelationshipsProps } from "../types";
|
||||
|
||||
export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||
const { companyId, csrfToken, assetId } = props;
|
||||
const { companyId, csrfToken, assetId, compact = false } = props;
|
||||
const { query, adding, setAdding, setEndingId, ending, active, ended, dateOf } =
|
||||
useAssetRelationshipsController(props);
|
||||
const { t } = useTranslation("assets");
|
||||
@ -20,7 +21,12 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||
<li className="space-y-2 rounded-md border p-3 text-sm" key={link.id}>
|
||||
<p className="font-medium">
|
||||
{source ? link.values.name : link.values.inverseName} ·{" "}
|
||||
{nameOf(source ? link.values.targetId : link.values.sourceId)}
|
||||
<BrowserLink
|
||||
className="cursor-pointer text-primary underline-offset-4 hover:underline"
|
||||
href={`/companies/${companyId}/assets/${source ? link.values.targetId : link.values.sourceId}`}
|
||||
>
|
||||
{nameOf(source ? link.values.targetId : link.values.sourceId)}
|
||||
</BrowserLink>
|
||||
</p>
|
||||
{link.values.notes && (
|
||||
<p className="whitespace-pre-wrap break-words">{link.values.notes}</p>
|
||||
@ -49,7 +55,7 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||
return (
|
||||
<section aria-label={t("relations")} className="space-y-3">
|
||||
<header className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="font-medium">{t("relations")}</h3>
|
||||
{!compact && <h3 className="font-medium">{t("relations")}</h3>}
|
||||
<Button onClick={() => setAdding(true)} size="sm" variant="outline">
|
||||
{t("linkAsset")}
|
||||
</Button>
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { BrowserLink } from "@metis/core/client";
|
||||
import { DetailField } from "@repo/rdx-ui/components/detail-field";
|
||||
import { DetailSection } from "@repo/rdx-ui/components/detail-section";
|
||||
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -22,49 +25,78 @@ export function AssetDetail(props: AssetDetailProps) {
|
||||
? t(value ? "yes" : "no")
|
||||
: String(value);
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
{!compact && <p className="text-sm text-muted-foreground">{t("snapshotNotice")}</p>}
|
||||
<dl className={compact ? "grid gap-4" : "grid gap-3 sm:grid-cols-2"}>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">{t("template")}</dt>
|
||||
<dd>{asset.specification.templateName}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">{t("status")}</dt>
|
||||
<dd>{t(asset.values.status)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">{t("site")}</dt>
|
||||
<dd>{nameOf(asset.values.siteId, lookups.sites)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">{t("person")}</dt>
|
||||
<dd>{nameOf(asset.values.personId, lookups.people)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">{t("workstationInAsset")}</dt>
|
||||
<dd>{workstations?.map((item) => item.values.name).join(" · ") || t("noWorkstation")}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<FieldSections
|
||||
items={asset.specification.fields}
|
||||
renderItem={(field) => (
|
||||
<div key={field.fieldId}>
|
||||
<dt className="text-sm text-muted-foreground">
|
||||
{field.name}
|
||||
{field.unit ? ` (${field.unit})` : ""}
|
||||
</dt>
|
||||
<dd className="whitespace-pre-wrap break-words">
|
||||
<DetailSection title={t("detailSections.identification")}>
|
||||
<dl>
|
||||
<DetailField label={t("template")}>
|
||||
<BrowserLink href={`/companies/${asset.companyId}/catalogs/assets?tab=templates`}>
|
||||
{asset.specification.templateName}
|
||||
</BrowserLink>
|
||||
</DetailField>
|
||||
<DetailField label={t("status")}>{t(asset.values.status)}</DetailField>
|
||||
</dl>
|
||||
</DetailSection>
|
||||
<DetailSection title={t("detailSections.assignment")}>
|
||||
<dl>
|
||||
<DetailField label={t("site")}>
|
||||
{asset.values.siteId ? (
|
||||
<BrowserLink href={`/companies/${asset.companyId}/sites`}>
|
||||
{nameOf(asset.values.siteId, lookups.sites)}
|
||||
</BrowserLink>
|
||||
) : (
|
||||
t("noAssignment")
|
||||
)}
|
||||
</DetailField>
|
||||
<DetailField label={t("person")}>
|
||||
{asset.values.personId ? (
|
||||
<BrowserLink href={`/companies/${asset.companyId}/people/${asset.values.personId}`}>
|
||||
{nameOf(asset.values.personId, lookups.people)}
|
||||
</BrowserLink>
|
||||
) : (
|
||||
t("noAssignment")
|
||||
)}
|
||||
</DetailField>
|
||||
<DetailField label={t("workstationInAsset")}>
|
||||
{workstations?.length
|
||||
? workstations.map((item) => (
|
||||
<BrowserLink
|
||||
className="block"
|
||||
href={`/companies/${asset.companyId}/workstations`}
|
||||
key={item.id}
|
||||
>
|
||||
{item.values.name}
|
||||
</BrowserLink>
|
||||
))
|
||||
: t("noWorkstation")}
|
||||
</DetailField>
|
||||
</dl>
|
||||
</DetailSection>
|
||||
<DetailSection title={t("characteristics")}>
|
||||
<FieldSections
|
||||
items={asset.specification.fields}
|
||||
renderItem={(field) => (
|
||||
<DetailField
|
||||
key={field.fieldId}
|
||||
label={
|
||||
<>
|
||||
{field.name}
|
||||
{field.unit ? ` (${field.unit})` : ""}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{display(asset.values.characteristics[field.fieldId])}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
sections={asset.specification.sections}
|
||||
ungroupedLabel={t("sections.ungrouped")}
|
||||
values
|
||||
/>
|
||||
</DetailField>
|
||||
)}
|
||||
sections={asset.specification.sections}
|
||||
ungroupedLabel={t("sections.ungrouped")}
|
||||
values
|
||||
/>
|
||||
</DetailSection>
|
||||
{asset.values.notes && (
|
||||
<p className="whitespace-pre-wrap break-words">{asset.values.notes}</p>
|
||||
<DetailSection title={t("detailSections.notes")}>
|
||||
<p className="whitespace-pre-wrap break-words text-sm">{asset.values.notes}</p>
|
||||
</DetailSection>
|
||||
)}
|
||||
{children}
|
||||
{!compact && (
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { DetailSection } from "@repo/rdx-ui/components/detail-section";
|
||||
import { XIcon } from "@repo/rdx-ui/lib/icons";
|
||||
import { Button } from "@repo/shadcn-ui/components/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -36,31 +38,41 @@ export function AssetSelectionPanel({
|
||||
return (
|
||||
<aside
|
||||
aria-label={t("assetPanel")}
|
||||
className="hidden min-w-0 grid-cols-[minmax(0,1fr)] gap-6 rounded-panel border border-border bg-muted p-5 [overflow-wrap:anywhere] inventory:grid"
|
||||
className="@container hidden min-w-0 grid-cols-[minmax(0,1fr)] gap-4 rounded-panel border border-border bg-card p-4 [overflow-wrap:anywhere] inventory:grid"
|
||||
>
|
||||
<header className="flex items-start justify-between gap-3">
|
||||
<h2 className="min-w-0 break-words text-lg font-semibold">{asset.values.name}</h2>
|
||||
<Button aria-label={t("closeAssetPanel")} onClick={onClose} size="sm" variant="ghost">
|
||||
×
|
||||
<Button aria-label={t("closeAssetPanel")} onClick={onClose} size="icon-sm" variant="ghost">
|
||||
<XIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
</header>
|
||||
<AssetDetail asset={asset} compact lookups={lookups}>
|
||||
<AssetRelationships assetId={asset.id} companyId={companyId} csrfToken={csrfToken} />
|
||||
<AssetSupport
|
||||
assetId={asset.id}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
retired={asset.values.status === "retired"}
|
||||
/>
|
||||
</AssetDetail>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={() => onView(asset.id)} variant="outline">
|
||||
<Button onClick={() => onView(asset.id)} size="sm" variant="outline">
|
||||
{t("viewAsset")}
|
||||
</Button>
|
||||
<Button disabled={!canEdit} onClick={() => onEdit(asset.id)} variant="outline">
|
||||
<Button disabled={!canEdit} onClick={() => onEdit(asset.id)} size="sm" variant="outline">
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</div>
|
||||
<AssetDetail asset={asset} compact lookups={lookups}>
|
||||
<DetailSection title={t("relations")}>
|
||||
<AssetRelationships
|
||||
assetId={asset.id}
|
||||
compact
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
/>
|
||||
</DetailSection>
|
||||
<DetailSection title={t("support.title")}>
|
||||
<AssetSupport
|
||||
assetId={asset.id}
|
||||
compact
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
retired={asset.values.status === "retired"}
|
||||
/>
|
||||
</DetailSection>
|
||||
</AssetDetail>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-[minmax(0,1fr)] items-start gap-6 inventory:grid-cols-[minmax(0,1fr)_280px] inventory:data-[detail-open=false]:grid-cols-[minmax(0,1fr)]"
|
||||
className="grid grid-cols-[minmax(0,1fr)] items-start gap-6 inventory:grid-cols-[minmax(0,1fr)_clamp(320px,24vw,400px)] inventory:data-[detail-open=false]:grid-cols-[minmax(0,1fr)]"
|
||||
data-detail-open={detailOpen && Boolean(selected)}
|
||||
data-slot="inventory-layout"
|
||||
>
|
||||
|
||||
@ -13,6 +13,7 @@ import { buildCatalogResponse } from "../../../api/application/asset-catalog-res
|
||||
import { AssetCatalog } from "../../../api/domain/asset-catalog.aggregate";
|
||||
import { seedAssetCatalog } from "../../../api/domain/asset-catalog-seed";
|
||||
import type { RelationshipDTO } from "../../../common";
|
||||
import { AssetSchema } from "../../../common/inventory";
|
||||
import { AssetRelationships } from "../../asset-relationships";
|
||||
import { catalogKeys } from "../../catalog/shared/api";
|
||||
import { CatalogRelationForm } from "../../catalog-relations";
|
||||
@ -20,6 +21,7 @@ import { CatalogTemplateForm } from "../../catalog-templates";
|
||||
import { assetsEnglish, assetsSpanish } from "../../shared";
|
||||
import { inventoryKeys } from "../../shared/queries";
|
||||
import { WorkstationForm, WorkstationsPage } from "../../workstations";
|
||||
import { AssetSelectionPanel } from "../detail/ui/asset-selection-panel";
|
||||
import { AssetInventoryPage } from "../list";
|
||||
|
||||
import { AssetForm } from "./ui";
|
||||
@ -218,3 +220,80 @@ test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no
|
||||
assert.ok(!nested.includes(`aria-label="${label}"`));
|
||||
}
|
||||
});
|
||||
|
||||
test("la ficha lateral agrupa información y ofrece acciones superiores y enlaces reales", async () => {
|
||||
const fieldId = UniqueID.generateNewID().toPrimitive();
|
||||
const personId = UniqueID.generateNewID().toPrimitive();
|
||||
const siteId = UniqueID.generateNewID().toPrimitive();
|
||||
const asset = AssetSchema.parse({
|
||||
id: UniqueID.generateNewID().toPrimitive(),
|
||||
companyId,
|
||||
specification: {
|
||||
templateId: fieldId,
|
||||
templateName: "Impresora",
|
||||
fields: [
|
||||
{ fieldId, name: "Número de serie", type: "text", unit: "", choices: [], required: false },
|
||||
],
|
||||
},
|
||||
values: {
|
||||
name: "Impresora de administración",
|
||||
templateId: fieldId,
|
||||
siteId,
|
||||
personId,
|
||||
status: "in_use",
|
||||
notes: "Uso compartido",
|
||||
characteristics: { [fieldId]: "ABC123" },
|
||||
},
|
||||
history: [
|
||||
{
|
||||
id: fieldId,
|
||||
actorId: personId,
|
||||
actorName: "Técnico",
|
||||
recordedAt: "2026-10-09T10:00:00.000Z",
|
||||
values: {
|
||||
name: "Impresora de administración",
|
||||
templateId: fieldId,
|
||||
siteId,
|
||||
personId,
|
||||
status: "in_use",
|
||||
notes: "Uso compartido",
|
||||
characteristics: { [fieldId]: "ABC123" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const html = await render(
|
||||
"es",
|
||||
<AssetSelectionPanel
|
||||
asset={asset}
|
||||
canEdit
|
||||
companyId={companyId}
|
||||
csrfToken="test"
|
||||
lookups={{
|
||||
...lookups,
|
||||
people: [{ id: personId, name: "Ana", active: true }],
|
||||
sites: [{ id: siteId, name: "Fábrica" }],
|
||||
}}
|
||||
onClose={() => undefined}
|
||||
onEdit={() => undefined}
|
||||
onView={() => undefined}
|
||||
open
|
||||
/>
|
||||
);
|
||||
assert.ok(html.indexOf("Ficha e historial") < html.indexOf("Identificación"));
|
||||
assert.ok(html.indexOf(">Editar<") < html.indexOf("Identificación"));
|
||||
for (const group of [
|
||||
"Identificación",
|
||||
"Ubicación y asignación",
|
||||
"Características",
|
||||
"Observaciones",
|
||||
]) {
|
||||
assert.ok(html.includes(group));
|
||||
}
|
||||
assert.ok(html.includes("<summary"));
|
||||
assert.ok(html.includes('open=""'));
|
||||
assert.ok(html.includes(`href="/companies/${companyId}/people/${personId}"`));
|
||||
assert.ok(html.includes(`href="/companies/${companyId}/sites"`));
|
||||
assert.ok(html.includes(`href="/companies/${companyId}/catalogs/assets?tab=templates"`));
|
||||
assert.ok(html.includes("ABC123"));
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export type AssetSupportProps = {
|
||||
compact?: boolean;
|
||||
companyId: string;
|
||||
assetId: string;
|
||||
csrfToken: string;
|
||||
|
||||
@ -9,7 +9,7 @@ import { useAssetSupportController } from "../controllers";
|
||||
import type { AssetSupportProps } from "../types";
|
||||
|
||||
export function AssetSupport(props: AssetSupportProps) {
|
||||
const { companyId, assetId, csrfToken, retired = false } = props;
|
||||
const { companyId, assetId, csrfToken, retired = false, compact = false } = props;
|
||||
const {
|
||||
query,
|
||||
open,
|
||||
@ -27,9 +27,9 @@ export function AssetSupport(props: AssetSupportProps) {
|
||||
} = useAssetSupportController(props);
|
||||
const { t } = useTranslation("assets");
|
||||
return (
|
||||
<section className="space-y-3 rounded-md border p-4">
|
||||
<section className={compact ? "space-y-3" : "space-y-3 rounded-md border p-4"}>
|
||||
<header className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 className="font-semibold">{t("support.title")}</h3>
|
||||
{!compact && <h3 className="font-semibold">{t("support.title")}</h3>}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={() => setToolsOpen(true)} size="sm" variant="outline">
|
||||
{t("support.tools")}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { DetailField } from "@repo/rdx-ui/components/detail-field";
|
||||
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -35,15 +36,17 @@ export function ResourceDetail({
|
||||
<FieldSections
|
||||
items={resource.specification.fields}
|
||||
renderItem={(field) => (
|
||||
<div key={field.fieldId}>
|
||||
<dt className="text-sm text-muted-foreground">
|
||||
{field.name}
|
||||
{field.unit ? ` (${field.unit})` : ""}
|
||||
</dt>
|
||||
<dd className="whitespace-pre-wrap break-words">
|
||||
{display(resource.values.characteristics[field.fieldId])}
|
||||
</dd>
|
||||
</div>
|
||||
<DetailField
|
||||
key={field.fieldId}
|
||||
label={
|
||||
<>
|
||||
{field.name}
|
||||
{field.unit ? ` (${field.unit})` : ""}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{display(resource.values.characteristics[field.fieldId])}
|
||||
</DetailField>
|
||||
)}
|
||||
sections={resource.specification.sections}
|
||||
ungroupedLabel={t("sections.ungrouped")}
|
||||
|
||||
13
packages/rdx-ui/src/components/detail-field.tsx
Normal file
13
packages/rdx-ui/src/components/detail-field.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/** Etiqueta y valor responden al ancho de su ficha, no al ancho total del navegador. */
|
||||
export function DetailField({ label, children }: { label: ReactNode; children: ReactNode }) {
|
||||
return (
|
||||
<div className="grid min-w-0 gap-1 border-b border-border/60 py-2.5 first:pt-0 last:border-0 last:pb-0 @min-[240px]:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] @min-[240px]:gap-3">
|
||||
<dt className="text-xs leading-5 text-muted-foreground">{label}</dt>
|
||||
<dd className="min-w-0 whitespace-pre-wrap break-words text-sm font-medium leading-5 [&_a]:cursor-pointer [&_a]:text-primary [&_a]:underline-offset-4 [&_a:hover]:underline [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-ring">
|
||||
{children}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
packages/rdx-ui/src/components/detail-section.tsx
Normal file
13
packages/rdx-ui/src/components/detail-section.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/** Agrupación plegable accesible para fichas; comienza abierta sin ocultar información. */
|
||||
export function DetailSection({ title, children }: { title: string; children: ReactNode }) {
|
||||
return (
|
||||
<details className="@container border-t border-border/60 first:border-t-0" open>
|
||||
<summary className="cursor-pointer py-2 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-ring">
|
||||
{title}
|
||||
</summary>
|
||||
<div className="space-y-3 pb-2 pt-1">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@ -29,9 +29,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
||||
}, [hasErrors]);
|
||||
const content = (fields: readonly T[]) =>
|
||||
values ? (
|
||||
<dl className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
||||
{fields.map(renderItem)}
|
||||
</dl>
|
||||
<dl className="@container">{fields.map(renderItem)}</dl>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
||||
{fields.map(renderItem)}
|
||||
@ -62,11 +60,17 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<details className="@container rounded-md border" key={section.id} open>
|
||||
<summary className="cursor-pointer rounded-md px-4 py-3 text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring">
|
||||
<details
|
||||
className={values ? "@container" : "@container rounded-md border"}
|
||||
key={section.id}
|
||||
open
|
||||
>
|
||||
<summary
|
||||
className={`cursor-pointer text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring ${values ? "py-2" : "rounded-md px-4 py-3"}`}
|
||||
>
|
||||
{section.name}
|
||||
</summary>
|
||||
<div className="space-y-3 border-t p-4">
|
||||
<div className={values ? "space-y-3 pt-1" : "space-y-3 border-t p-4"}>
|
||||
{section.description && (
|
||||
<p className="text-sm text-muted-foreground">{section.description}</p>
|
||||
)}
|
||||
|
||||
@ -23,4 +23,5 @@ export {
|
||||
UserRoundXIcon,
|
||||
UsersIcon,
|
||||
WrenchIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user