metis-desk/packages/rdx-ui/src/components/template-sections-editor.tsx

173 lines
5.4 KiB
TypeScript
Raw Normal View History

import { Button } from "@repo/shadcn-ui/components/button";
import { PlusIcon } from "lucide-react";
import { useId } from "react";
import { useDragReorder } from "../hooks/use-drag-reorder";
import { removeTemplateSection } from "../lib/template-sections";
import type { FieldSection } from "./field-sections";
import { FormSectionTitle } from "./form-section-title";
import { ReorderDropIndicator } from "./reorder-drop-indicator";
import { ReorderHandle } from "./reorder-handle";
import { TemplateSectionEditor } from "./template-section-editor";
import { TemplateSectionFields } from "./template-section-fields";
export type SectionBinding = { fieldId: string; required: boolean; sectionId?: string | undefined };
2026-10-10 10:01:58 +00:00
export type SectionEditorLabels = {
title: string;
expand: string;
collapse: string;
fields: string;
reorder: string;
reorderHelp: string;
add: string;
name: string;
description: string;
ungrouped: string;
moveUp: string;
moveDown: string;
remove: string;
removeField: string;
required: string;
assign: string;
help: string;
empty: string;
};
export function TemplateSectionsEditor({
sections,
fields,
fieldNames,
fieldNotes,
labels,
disabled = false,
createId,
onChange,
addFieldAction,
fieldLayout = "cards",
sectionLayout = "stack",
}: {
fieldLayout?: "cards" | "compact";
sectionLayout?: "stack" | "responsive";
sections: readonly FieldSection[];
fields: readonly SectionBinding[];
fieldNames: Readonly<Record<string, string>>;
fieldNotes?: Readonly<Record<string, string>> | undefined;
labels: SectionEditorLabels;
disabled?: boolean;
createId: () => string;
addFieldAction?: { label: string; onClick: (sectionId: string | undefined) => void };
onChange: (value: { sections: FieldSection[]; fields: SectionBinding[] }) => void;
}) {
const prefix = useId();
2026-10-10 10:01:58 +00:00
const saveSections = (next: FieldSection[]) => onChange({ sections: next, fields: [...fields] });
2026-10-10 10:01:58 +00:00
const { getHandleProps, getItemProps, getDropPlacement } = useDragReorder({
items: sections,
getId: (section) => section.id,
disabled,
onChange: saveSections,
});
const moveSection = (index: number, offset: number) => {
const next = [...sections];
const first = next[index];
const second = next[index + offset];
2026-10-10 10:01:58 +00:00
if (first && second) {
next[index] = second;
next[index + offset] = first;
saveSections(next);
}
};
2026-10-10 10:01:58 +00:00
const removeSection = (id: string) => onChange(removeTemplateSection(sections, fields, id));
2026-10-10 10:01:58 +00:00
const fieldList = (sectionId?: string | undefined) => (
<TemplateSectionFields
addAction={
addFieldAction
? {
label: addFieldAction.label,
onClick: () => addFieldAction.onClick(sectionId),
}
: undefined
}
disabled={disabled}
fieldNames={fieldNames}
fieldNotes={fieldNotes}
fields={fields}
labels={labels}
layout={fieldLayout}
onChange={(next) => onChange({ sections: [...sections], fields: next })}
sectionId={sectionId}
sections={sections}
/>
);
2026-10-10 10:01:58 +00:00
return (
<section aria-labelledby={`${prefix}-title`} className="@container/template-sections space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<FormSectionTitle id={`${prefix}-title`}>{labels.title}</FormSectionTitle>
<Button
disabled={disabled || sections.length >= 30}
onClick={() => saveSections([...sections, { id: createId(), name: "", description: "" }])}
size="sm"
type="button"
variant="outline"
>
<PlusIcon aria-hidden="true" />
{labels.add}
</Button>
</div>
<p className="sr-only" id={`${prefix}-reorder-help`}>
{labels.reorderHelp}
</p>
<div
className={
sectionLayout === "responsive"
? "grid grid-cols-1 items-stretch gap-4 @min-[1000px]/template-sections:grid-cols-2"
: "space-y-5"
}
>
2026-10-09 18:30:41 +00:00
{sections.map((section, index) => (
<fieldset
{...getItemProps(section.id)}
className="relative min-w-0 overflow-hidden rounded-lg border border-border bg-muted/30 data-[dragging=true]:opacity-50"
2026-10-09 18:30:41 +00:00
disabled={disabled}
key={section.id}
>
<legend className="sr-only">{section.name || labels.name}</legend>
<ReorderDropIndicator placement={getDropPlacement(section.id)} />
<TemplateSectionEditor
disabled={disabled}
dragHandle={
<ReorderHandle
{...getHandleProps(section.id)}
aria-describedby={`${prefix}-reorder-help`}
label={`${labels.reorder}: ${section.name || labels.name} (${index + 1}/${sections.length})`}
2026-10-09 18:30:41 +00:00
/>
}
index={index}
labels={labels}
onChange={(next) =>
saveSections(sections.map((item) => (item.id === section.id ? next : item)))
}
onMove={(offset) => moveSection(index, offset)}
onRemove={() => removeSection(section.id)}
section={section}
sectionCount={sections.length}
>
{fieldList(section.id)}
</TemplateSectionEditor>
2026-10-09 18:30:41 +00:00
</fieldset>
))}
</div>
{fieldList()}
</section>
);
}