Render forms from indexed form elements
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 05:30:53 +03:00
parent d26aaef44a
commit 5bd188fe6f
5 changed files with 86 additions and 83 deletions
@@ -3049,7 +3049,7 @@ function FormDesignerPanel({
const form = objectForms.find((item) => item.form.lineage_id === selectedFormId) ?? objectForms[0]; const form = objectForms.find((item) => item.form.lineage_id === selectedFormId) ?? objectForms[0];
const commands = form?.commands.slice(0, 6) ?? []; const commands = form?.commands.slice(0, 6) ?? [];
const formKey = form?.form.lineage_id ?? "draft"; const formKey = form?.form.lineage_id ?? "draft";
const baseElements = useMemo(() => buildIdeFormElements(data, form), [data, form]); const baseElements = useMemo(() => buildIdeFormElements(form), [form]);
const elements = elementDrafts[formKey] ?? baseElements; const elements = elementDrafts[formKey] ?? baseElements;
const formTitle = titleByForm[formKey] ?? form?.form.name ?? "ФормаДокумента"; const formTitle = titleByForm[formKey] ?? form?.form.name ?? "ФормаДокумента";
const formObjectCaption = language === "ru" ? `${formTitle} (форма 1С 8.5)` : `${formTitle} (1C 8.5 form)`; const formObjectCaption = language === "ru" ? `${formTitle} (форма 1С 8.5)` : `${formTitle} (1C 8.5 form)`;
@@ -3057,9 +3057,9 @@ function FormDesignerPanel({
useEffect(() => { useEffect(() => {
if (form && !elementDrafts[form.form.lineage_id]) { if (form && !elementDrafts[form.form.lineage_id]) {
setElementDrafts((current) => ({ ...current, [form.form.lineage_id]: buildIdeFormElements(data, form) })); setElementDrafts((current) => ({ ...current, [form.form.lineage_id]: buildIdeFormElements(form) }));
} }
}, [data, elementDrafts, form]); }, [elementDrafts, form]);
const updateElement = (id: string, patch: Partial<IdeFormElementDraft>) => { const updateElement = (id: string, patch: Partial<IdeFormElementDraft>) => {
setElementDrafts((current) => ({ setElementDrafts((current) => ({
@@ -3175,9 +3175,23 @@ function FormDesignerPanel({
"grid grid-cols-12 gap-x-3 gap-y-2 p-5", "grid grid-cols-12 gap-x-3 gap-y-2 p-5",
layout === "compact" ? "gap-y-1" : "" layout === "compact" ? "gap-y-1" : ""
].join(" ")}> ].join(" ")}>
{elements.map((element) => ( {elements.length ? (
elements.map((element) => (
<IdeFormControl element={element} forceHalf={layout === "columns"} key={element.id} /> <IdeFormControl element={element} forceHalf={layout === "columns"} key={element.id} />
))} ))
) : (
<div className="col-span-12 border border-dashed border-[#aeb8c6] bg-white px-4 py-6 text-sm text-slate-600" data-ide-form-empty>
<div className="font-semibold text-slate-900">{language === "ru" ? "Структура элементов формы не загружена" : "Form element structure is not loaded"}</div>
<div className="mt-1 text-xs">
{form?.form.qualified_name ?? form?.form.name ?? data.projectId}
</div>
<div className="mt-3 text-xs">
{language === "ru"
? "В текущем индексе для этой формы нет узлов элементов. SFERA не подставляет шаблонные поля, чтобы не искажать объект 1С."
: "The current index has no element nodes for this form. SFERA does not insert template fields because that would distort the 1C object."}
</div>
</div>
)}
</div> </div>
<div className="mt-auto border-t border-[#ccd4df] bg-white px-4 py-3"> <div className="mt-auto border-t border-[#ccd4df] bg-white px-4 py-3">
@@ -3306,7 +3320,7 @@ function ideFormControlInput(element: IdeFormElementDraft) {
if (element.controlKind === "text") { if (element.controlKind === "text") {
return <textarea className="min-h-16 resize-none border border-[#aeb8c6] bg-white px-2 py-1 text-xs" readOnly value={element.binding} />; return <textarea className="min-h-16 resize-none border border-[#aeb8c6] bg-white px-2 py-1 text-xs" readOnly value={element.binding} />;
} }
return <input className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" readOnly value={element.controlKind === "date" ? "21.05.2026 0:00:00" : element.binding} />; return <input className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" readOnly value={element.binding} />;
} }
function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number }>) { function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number }>) {
@@ -3318,7 +3332,7 @@ function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number
); );
} }
function buildIdeFormElements(data: ProjectWorkspaceData, form: ProjectWorkspaceData["forms"][number] | undefined): IdeFormElementDraft[] { function buildIdeFormElements(form: ProjectWorkspaceData["forms"][number] | undefined): IdeFormElementDraft[] {
const explicitElements = form?.elements ?? []; const explicitElements = form?.elements ?? [];
if (explicitElements.length) { if (explicitElements.length) {
return explicitElements.map((element, index) => ({ return explicitElements.map((element, index) => ({
@@ -3330,38 +3344,7 @@ function buildIdeFormElements(data: ProjectWorkspaceData, form: ProjectWorkspace
width: "stretch" width: "stretch"
})); }));
} }
const attributes = data.selectedObjectSchema?.attributes ?? []; return [];
const tabularSections = data.selectedObjectSchema?.tabular_sections ?? [];
const fromSchema: IdeFormElementDraft[] = [
...attributes.map((attribute, index) => ({
id: attribute.lineage_id || `attribute.${index}`,
name: attribute.name,
caption: attribute.name,
controlKind: controlKindForFormNode(attribute.name, attribute.kind),
binding: attribute.name,
width: index < 2 ? "half" : "stretch"
} satisfies IdeFormElementDraft)),
...tabularSections.map((section, index) => ({
id: section.tabular_section.lineage_id || `table.${index}`,
name: section.tabular_section.name,
caption: section.tabular_section.name,
controlKind: "table" as const,
binding: section.tabular_section.name,
width: "stretch" as const
}))
];
if (fromSchema.length) {
return fromSchema;
}
const formName = form?.form.name ?? "ФормаДокумента";
if (formName.includes("ФормаДокумента")) {
return [
{ id: "default.number", name: "Номер", caption: "Номер", controlKind: "input", binding: "Объект.Номер", width: "half" },
{ id: "default.date", name: "Дата", caption: "Дата", controlKind: "date", binding: "Объект.Дата", width: "half" },
{ id: "default.table", name: "Товары", caption: "Товары", controlKind: "table", binding: "Объект.Товары", width: "stretch" }
];
}
return [{ id: "default.name", name: "Наименование", caption: "Наименование", controlKind: "input", binding: "Объект.Наименование", width: "stretch" }];
} }
function controlKindForFormNode(name: string, kind: string): IdeFormElementDraft["controlKind"] { function controlKindForFormNode(name: string, kind: string): IdeFormElementDraft["controlKind"] {
@@ -5531,7 +5531,7 @@ function FormDesignerPanel({ projectId, object }: Readonly<{ projectId: string;
const [extraElementsByForm, setExtraElementsByForm] = useState<Record<string, FormDesignerElementDraft[]>>({}); const [extraElementsByForm, setExtraElementsByForm] = useState<Record<string, FormDesignerElementDraft[]>>({});
const [newElementName, setNewElementName] = useState(""); const [newElementName, setNewElementName] = useState("");
const [newElementKind, setNewElementKind] = useState<FormDesignerElementDraft["kind"]>("input"); const [newElementKind, setNewElementKind] = useState<FormDesignerElementDraft["kind"]>("input");
const baseElements = activeForm ? buildFormDesignerElements(object, activeForm) : []; const baseElements = activeForm ? buildFormDesignerElements(activeForm) : [];
const elements = [...baseElements, ...(extraElementsByForm[activeFormKey] ?? [])].map((element) => ({ const elements = [...baseElements, ...(extraElementsByForm[activeFormKey] ?? [])].map((element) => ({
...element, ...element,
...(elementOverrides[element.id] ?? {}) ...(elementOverrides[element.id] ?? {})
@@ -5617,9 +5617,17 @@ function FormDesignerPanel({ projectId, object }: Readonly<{ projectId: string;
className={`grid grid-cols-12 gap-x-3 gap-y-2 p-4 ${layout === "compact" ? "gap-y-1" : ""}`} className={`grid grid-cols-12 gap-x-3 gap-y-2 p-4 ${layout === "compact" ? "gap-y-1" : ""}`}
data-legacy-form-layout={layout} data-legacy-form-layout={layout}
> >
{elements.map((element) => ( {elements.length ? (
elements.map((element) => (
<LegacyFormControl key={element.id} element={element} forceHalf={layout === "columns"} /> <LegacyFormControl key={element.id} element={element} forceHalf={layout === "columns"} />
))} ))
) : (
<div className="col-span-12 border border-dashed border-[#aeb8c6] bg-white px-4 py-6 text-sm text-muted-foreground">
<div className="font-semibold text-foreground">Структура элементов формы не загружена</div>
<div className="mt-1 text-xs">{formQualifiedName(object, activeForm)}</div>
<div className="mt-3 text-xs">SFERA не подставляет реквизиты объекта вместо элементов формы, чтобы не искажать объект 1С.</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -5763,29 +5771,48 @@ function legacyFormControlInput(element: FormDesignerElementDraft): ReactNode {
if (element.kind === "text") { if (element.kind === "text") {
return <textarea value={element.binding} readOnly className="min-h-16 resize-none border border-[#aeb8c6] bg-white px-2 py-1 text-xs" />; return <textarea value={element.binding} readOnly className="min-h-16 resize-none border border-[#aeb8c6] bg-white px-2 py-1 text-xs" />;
} }
return <input value={element.kind === "date" ? "21.05.2026 0:00:00" : element.binding} readOnly className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" />; return <input value={element.binding} readOnly className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" />;
} }
function buildFormDesignerElements(object: NormalizedMetadataObject, form: ObjectPart): FormDesignerElementDraft[] { function buildFormDesignerElements(form: ObjectPart): FormDesignerElementDraft[] {
const fields = [...object.attributes, ...object.tabular_sections]; const rawElements = form.attributes.elements;
if (fields.length) { if (Array.isArray(rawElements)) {
return fields.map((field, index) => ({ return rawElements
id: `${form.name}-${field.kind}-${field.name}`, .filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null)
name: field.name, .map((item, index) => {
caption: field.name, const name = String(item.name ?? item.caption ?? `Элемент${index + 1}`);
kind: field.kind === "TABULAR_SECTION" ? "table" : field.name.toLowerCase().includes("дата") ? "date" : "input", const kind = String(item.control_kind ?? item.control ?? item.type ?? item.kind ?? "");
binding: field.kind === "TABULAR_SECTION" ? `Объект.${field.name}` : field.name, return {
width: field.kind === "TABULAR_SECTION" ? "stretch" : index < 2 ? "half" : "stretch" id: String(item.lineage_id ?? item.id ?? `${form.name}-${name}-${index}`),
})); name,
caption: String(item.caption ?? name),
kind: formDesignerKindFor(kind, name),
binding: String(item.binding ?? item.path ?? name),
width: String(item.width ?? "") === "half" ? "half" : String(item.width ?? "") === "third" ? "third" : "stretch"
};
});
} }
if (form.name.includes("ФормаДокумента")) { return [];
return [
{ id: `${form.name}-number`, name: "Номер", caption: "Номер", kind: "input", binding: "Объект.Номер", width: "half" },
{ id: `${form.name}-date`, name: "Дата", caption: "Дата", kind: "date", binding: "Объект.Дата", width: "half" },
{ id: `${form.name}-table`, name: "Товары", caption: "Товары", kind: "table", binding: "Объект.Товары", width: "stretch" }
];
} }
return [{ id: `${form.name}-name`, name: "Наименование", caption: "Наименование", kind: "input", binding: "Объект.Наименование", width: "stretch" }];
function formDesignerKindFor(kind: string, name: string): FormDesignerElementDraft["kind"] {
const raw = `${kind} ${name}`.toLowerCase();
if (raw.includes("table") || raw.includes("табли") || raw.includes("список")) {
return "table";
}
if (raw.includes("check") || raw.includes("boolean") || raw.includes("флаж") || raw.includes("булево")) {
return "checkbox";
}
if (raw.includes("date") || raw.includes("дата")) {
return "date";
}
if (raw.includes("group") || raw.includes("груп")) {
return "group";
}
if (raw.includes("text") || raw.includes("надпись")) {
return "text";
}
return "input";
} }
function formQualifiedName(object: NormalizedMetadataObject, form: ObjectPart): string { function formQualifiedName(object: NormalizedMetadataObject, form: ObjectPart): string {
@@ -21,8 +21,6 @@ def build_form_editor_draft(
commands=_commands_from_semantics(form_semantics), commands=_commands_from_semantics(form_semantics),
elements=_elements_from_semantics(form_semantics), elements=_elements_from_semantics(form_semantics),
) )
if not draft.elements:
draft.elements = _default_elements_for_form(draft)
if form_data: if form_data:
_apply_form_data(draft, form_data) _apply_form_data(draft, form_data)
return draft return draft
@@ -90,22 +88,6 @@ def _apply_form_data(draft: FormEditorDraft, form_data: dict[str, list[str]]) ->
) )
def _default_elements_for_form(draft: FormEditorDraft) -> list[FormEditorElement]:
if "ФормаСписка" in draft.form_name:
return [
FormEditorElement("default.list", "Список", f"{draft.form_name}.Список", "Список", "table", "Список", row=0),
]
if "ФормаДокумента" in draft.form_name:
return [
FormEditorElement("default.number", "Номер", f"{draft.form_name}.Номер", "Номер", "input", "Объект.Номер", "half", 0),
FormEditorElement("default.date", "Дата", f"{draft.form_name}.Дата", "Дата", "date", "Объект.Дата", "half", 1),
FormEditorElement("default.table", "Товары", f"{draft.form_name}.Товары", "Товары", "table", "Объект.Товары", row=2),
]
return [
FormEditorElement("default.name", "Наименование", f"{draft.form_name}.Наименование", "Наименование", "input", "Объект.Наименование", row=0),
]
def _control_kind_for(name: str, attributes: dict) -> str: def _control_kind_for(name: str, attributes: dict) -> str:
raw = str(attributes.get("control") or attributes.get("control_kind") or attributes.get("type") or "").casefold() raw = str(attributes.get("control") or attributes.get("control_kind") or attributes.get("type") or "").casefold()
if "table" in raw or "табли" in raw: if "table" in raw or "табли" in raw:
@@ -235,7 +235,7 @@ def _canvas_window(draft: FormEditorDraft) -> str:
{''.join(_canvas_command(command) for command in draft.commands) or '<span class="muted">Команды не описаны</span>'} {''.join(_canvas_command(command) for command in draft.commands) or '<span class="muted">Команды не описаны</span>'}
</div> </div>
<div class="form-fields"> <div class="form-fields">
{''.join(_canvas_element(element) for element in draft.elements)} {''.join(_canvas_element(element) for element in draft.elements) or _empty_form_structure(draft)}
</div> </div>
</div> </div>
""" """
@@ -265,6 +265,16 @@ def _canvas_element(element: FormEditorElement) -> str:
""" """
def _empty_form_structure(draft: FormEditorDraft) -> str:
metadata = f"{draft.form_name} · {draft.owner_name}"
return f"""
<div class="form-empty-structure" data-html5-form-empty>
<strong>Структура элементов формы не загружена</strong>
<span>{escape(metadata)}</span>
</div>
"""
def _control_markup(element: FormEditorElement) -> str: def _control_markup(element: FormEditorElement) -> str:
if element.control_kind == "table": if element.control_kind == "table":
return f""" return f"""
@@ -277,7 +287,7 @@ def _control_markup(element: FormEditorElement) -> str:
if element.control_kind == "checkbox": if element.control_kind == "checkbox":
return f'<label class="form-check-control"><input type="checkbox" checked /> <span>{escape(element.binding)}</span></label>' return f'<label class="form-check-control"><input type="checkbox" checked /> <span>{escape(element.binding)}</span></label>'
if element.control_kind == "date": if element.control_kind == "date":
return '<input class="form-input-control" value="21.05.2026 0:00:00" readonly />' return f'<input class="form-input-control" value="{escape(element.binding)}" readonly />'
if element.control_kind == "group": if element.control_kind == "group":
return f'<div class="form-group-control">{escape(element.binding)}</div>' return f'<div class="form-group-control">{escape(element.binding)}</div>'
if element.control_kind == "text": if element.control_kind == "text":
@@ -9,6 +9,7 @@
.setup-layout{display:grid;grid-template-columns:340px minmax(0,1fr);gap:16px;max-width:1180px;margin:18px auto;padding:0 16px}.setup-workspace{background:#f3f6fb}.setup-card{padding:16px;border-bottom:1px solid var(--line)}.setup-card h1{margin:0;font-size:24px}.setup-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.setup-main{min-height:620px}.settings-form{display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:8px;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fff}.settings-form label{display:grid;gap:4px;font-size:12px;font-weight:800;color:var(--muted);text-transform:uppercase}.settings-form input{height:32px;border:1px solid var(--line);padding:0 8px}.saved{float:right;color:var(--ok);font-weight:900}.setup-actions-panel,.ops-filter{display:flex;gap:8px;flex-wrap:wrap;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fbfcfe}.ops-filter input{height:32px;min-width:180px;border:1px solid var(--line);padding:0 8px}.inline-form{display:flex;gap:8px;align-items:end}.inline-form label{display:grid;gap:4px;font-size:12px;font-weight:800;color:var(--muted);text-transform:uppercase}.inline-form select{height:32px;min-width:240px;border:1px solid var(--line);background:#fff;padding:0 8px}.rollback-form,.authoring-preview-form{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.rollback-form input,.authoring-preview-form input,.authoring-preview-form textarea{min-width:0;border:1px solid var(--line);padding:0 8px}.rollback-form input,.authoring-preview-form input{height:32px}.authoring-preview-form textarea{grid-column:1/-1;min-height:88px;padding:8px;resize:vertical;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace}.rollback-form button,.authoring-preview-form button{grid-column:1/-1}.setup-summary{display:grid;gap:0}.compact-lead{margin:0;padding:0 16px 16px}.setup-metrics,.ops-summary{display:grid;grid-template-columns:repeat(4,1fr);margin:0;border-top:1px solid var(--line)}.ops-summary{grid-template-columns:repeat(5,1fr);margin:12px 0}.setup-metrics div,.ops-summary div{padding:16px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);background:#fff}.setup-metrics div:last-child,.ops-summary div:last-child{border-right:0}.setup-metrics dt,.ops-summary dt{font-size:12px;color:var(--muted)}.setup-metrics dd,.ops-summary dd{margin:4px 0 0;font-size:28px;font-weight:900}.status-pill{border:1px solid var(--line);padding:6px 10px;background:#eef6f1;color:var(--ok);font-weight:800}.flush{border-top:1px solid var(--line)}.padded{padding:12px 16px;margin:0}.setup-detail,.history-item,.source-card,.check-item{display:grid;gap:3px;padding:12px 16px;border-bottom:1px solid var(--line)}.setup-detail span,.setup-detail small,.history-item span,.history-item small,.source-card span,.source-card small,.check-item span,.check-item small,.check-head span,.check-head small{color:var(--muted)}.source-list,.history-list,.check-list{display:grid}.check-head{display:flex;gap:10px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line);background:#fff}.job-log{margin:0;padding:0;list-style:none}.job-log li{padding:8px 16px;border-bottom:1px solid var(--line);color:var(--muted);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px} .setup-layout{display:grid;grid-template-columns:340px minmax(0,1fr);gap:16px;max-width:1180px;margin:18px auto;padding:0 16px}.setup-workspace{background:#f3f6fb}.setup-card{padding:16px;border-bottom:1px solid var(--line)}.setup-card h1{margin:0;font-size:24px}.setup-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.setup-main{min-height:620px}.settings-form{display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:8px;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fff}.settings-form label{display:grid;gap:4px;font-size:12px;font-weight:800;color:var(--muted);text-transform:uppercase}.settings-form input{height:32px;border:1px solid var(--line);padding:0 8px}.saved{float:right;color:var(--ok);font-weight:900}.setup-actions-panel,.ops-filter{display:flex;gap:8px;flex-wrap:wrap;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fbfcfe}.ops-filter input{height:32px;min-width:180px;border:1px solid var(--line);padding:0 8px}.inline-form{display:flex;gap:8px;align-items:end}.inline-form label{display:grid;gap:4px;font-size:12px;font-weight:800;color:var(--muted);text-transform:uppercase}.inline-form select{height:32px;min-width:240px;border:1px solid var(--line);background:#fff;padding:0 8px}.rollback-form,.authoring-preview-form{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.rollback-form input,.authoring-preview-form input,.authoring-preview-form textarea{min-width:0;border:1px solid var(--line);padding:0 8px}.rollback-form input,.authoring-preview-form input{height:32px}.authoring-preview-form textarea{grid-column:1/-1;min-height:88px;padding:8px;resize:vertical;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace}.rollback-form button,.authoring-preview-form button{grid-column:1/-1}.setup-summary{display:grid;gap:0}.compact-lead{margin:0;padding:0 16px 16px}.setup-metrics,.ops-summary{display:grid;grid-template-columns:repeat(4,1fr);margin:0;border-top:1px solid var(--line)}.ops-summary{grid-template-columns:repeat(5,1fr);margin:12px 0}.setup-metrics div,.ops-summary div{padding:16px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);background:#fff}.setup-metrics div:last-child,.ops-summary div:last-child{border-right:0}.setup-metrics dt,.ops-summary dt{font-size:12px;color:var(--muted)}.setup-metrics dd,.ops-summary dd{margin:4px 0 0;font-size:28px;font-weight:900}.status-pill{border:1px solid var(--line);padding:6px 10px;background:#eef6f1;color:var(--ok);font-weight:800}.flush{border-top:1px solid var(--line)}.padded{padding:12px 16px;margin:0}.setup-detail,.history-item,.source-card,.check-item{display:grid;gap:3px;padding:12px 16px;border-bottom:1px solid var(--line)}.setup-detail span,.setup-detail small,.history-item span,.history-item small,.source-card span,.source-card small,.check-item span,.check-item small,.check-head span,.check-head small{color:var(--muted)}.source-list,.history-list,.check-list{display:grid}.check-head{display:flex;gap:10px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line);background:#fff}.job-log{margin:0;padding:0;list-style:none}.job-log li{padding:8px 16px;border-bottom:1px solid var(--line);color:var(--muted);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px}
.form-editor-layout{grid-template-columns:280px minmax(0,1.2fr) minmax(320px,.8fr)}.form-editor-actions{display:flex;gap:8px;flex-wrap:wrap}.tree-item[data-html5-form-selected="true"]{background:#f8fbff;border-left-color:var(--brand)}.form-designer{height:calc(100% - 72px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden;background:#f7f9fc}.form-designer-head,.form-designer-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line);background:#fff}.form-designer-head strong,.form-designer-head small{display:block}.form-designer-head small{color:var(--muted)}.form-designer-head span{border:1px solid var(--line);background:#eef6f1;color:var(--ok);padding:5px 8px;font-size:12px;font-weight:900}.form-designer-foot{border-top:1px solid var(--line);border-bottom:0}.form-canvas{min-height:0;overflow:auto;padding:18px}.form-window{max-width:720px;min-height:420px;margin:0 auto;border:1px solid var(--line);background:#fff;box-shadow:0 8px 24px rgba(15,23,42,.08)}.form-window-title{height:38px;display:flex;align-items:center;border-bottom:1px solid var(--line);padding:0 12px;font-weight:900;background:#fbfcfe}.form-command-bar{display:flex;gap:6px;flex-wrap:wrap;padding:10px 12px;border-bottom:1px solid var(--line)}.form-command-bar button{height:28px;font-size:12px}.form-fields{display:grid;gap:10px;padding:14px}.form-field{display:grid;grid-template-columns:160px minmax(0,1fr);gap:8px;align-items:center}.form-field span{font-size:12px;font-weight:800;color:var(--muted)}.form-field input{height:30px;border:1px solid var(--line);padding:0 8px;background:#fbfaf7} .form-editor-layout{grid-template-columns:280px minmax(0,1.2fr) minmax(320px,.8fr)}.form-editor-actions{display:flex;gap:8px;flex-wrap:wrap}.tree-item[data-html5-form-selected="true"]{background:#f8fbff;border-left-color:var(--brand)}.form-designer{height:calc(100% - 72px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden;background:#f7f9fc}.form-designer-head,.form-designer-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line);background:#fff}.form-designer-head strong,.form-designer-head small{display:block}.form-designer-head small{color:var(--muted)}.form-designer-head span{border:1px solid var(--line);background:#eef6f1;color:var(--ok);padding:5px 8px;font-size:12px;font-weight:900}.form-designer-foot{border-top:1px solid var(--line);border-bottom:0}.form-canvas{min-height:0;overflow:auto;padding:18px}.form-window{max-width:720px;min-height:420px;margin:0 auto;border:1px solid var(--line);background:#fff;box-shadow:0 8px 24px rgba(15,23,42,.08)}.form-window-title{height:38px;display:flex;align-items:center;border-bottom:1px solid var(--line);padding:0 12px;font-weight:900;background:#fbfcfe}.form-command-bar{display:flex;gap:6px;flex-wrap:wrap;padding:10px 12px;border-bottom:1px solid var(--line)}.form-command-bar button{height:28px;font-size:12px}.form-fields{display:grid;gap:10px;padding:14px}.form-field{display:grid;grid-template-columns:160px minmax(0,1fr);gap:8px;align-items:center}.form-field span{font-size:12px;font-weight:800;color:var(--muted)}.form-field input{height:30px;border:1px solid var(--line);padding:0 8px;background:#fbfaf7}
.form-designer-body{min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 340px;overflow:hidden}.form-property-panel{min-height:0;overflow:auto;border-left:1px solid var(--line);background:#fff}.property-row{display:grid;grid-template-columns:1fr 132px;gap:8px;padding:12px;border-bottom:1px solid var(--line)}.property-row label,.form-editor-row label{display:grid;gap:4px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.property-row input,.property-row select,.form-editor-row input,.form-editor-row select,.form-add-row input,.form-add-row select{height:30px;min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.2 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.form-window{border-color:#b9c1cd;background:#fdfdfd;box-shadow:0 12px 28px rgba(33,43,54,.12)}.form-window-title{justify-content:space-between;height:34px;background:linear-gradient(#f8f9fb,#e9edf3);border-bottom-color:#cbd3df;color:#1f2937}.form-window-title small{font-size:11px;color:#687385;font-weight:800}.form-command-bar{min-height:42px;background:#f4f6f9;border-bottom-color:#ccd4df}.form-command-bar button{height:27px;border-color:#aeb8c6;background:linear-gradient(#fff,#edf1f6);box-shadow:inset 0 1px 0 #fff;color:#1f2937}.form-command-bar button:hover{background:#fff}.form-fields{grid-template-columns:repeat(12,minmax(0,1fr));align-content:start;gap:8px 12px;padding:14px 16px 22px;background:#fbfbfc}.form-field{grid-column:1/-1;grid-template-columns:150px minmax(0,1fr);min-height:32px;padding:4px 6px;border:1px solid transparent}.form-field:hover{border-color:#b9c1cd;background:#fff}.form-field[data-html5-form-width="half"]{grid-column:span 6}.form-field[data-html5-form-width="third"]{grid-column:span 4}.form-field>label{font-size:12px;font-weight:800;color:#4b5563;align-self:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.form-input-control,.form-text-control{width:100%;border:1px solid #aeb8c6;background:#fff;padding:0 7px;color:#17202a;box-shadow:inset 0 1px 2px rgba(15,23,42,.06)}.form-input-control{height:28px}.form-text-control{min-height:64px;resize:none}.form-check-control{display:flex;align-items:center;gap:8px;color:#374151}.form-check-control input{width:16px;height:16px}.form-table-control{border:1px solid #aeb8c6;background:#fff;min-height:108px;overflow:hidden}.form-table-control div{display:grid;grid-template-columns:2fr 1fr 1fr}.form-table-control span{min-height:27px;border-right:1px solid #d7dde6;border-bottom:1px solid #d7dde6;padding:5px 7px;color:#374151;font-size:12px}.form-table-control div:first-child span{background:#eef2f7;font-weight:900}.form-group-control{min-height:44px;border:1px dashed #aeb8c6;background:#f6f8fb;padding:10px;color:#687385;font-weight:800}.form-editor-row{display:grid;gap:7px;padding:10px 12px;border-bottom:1px solid var(--line)}label.form-editor-row{grid-template-columns:88px minmax(0,1fr);align-items:center;text-transform:none}.form-editor-row small{color:var(--muted);font-size:11px}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr 118px 1fr 104px}.form-add-row{display:grid;grid-template-columns:minmax(0,1fr) 118px;gap:8px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.button.primary{background:var(--brand);border-color:var(--brand);color:#fff;margin:12px}.form-designer-foot small{color:var(--muted);font-weight:800}.form-window[data-html5-form-layout="compact"] .form-fields{gap:4px 8px}.form-window[data-html5-form-layout="compact"] .form-field{min-height:28px}.form-window[data-html5-form-layout="columns"] .form-field{grid-column:span 6}.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="table"],.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="group"]{grid-column:1/-1} .form-designer-body{min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 340px;overflow:hidden}.form-property-panel{min-height:0;overflow:auto;border-left:1px solid var(--line);background:#fff}.property-row{display:grid;grid-template-columns:1fr 132px;gap:8px;padding:12px;border-bottom:1px solid var(--line)}.property-row label,.form-editor-row label{display:grid;gap:4px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.property-row input,.property-row select,.form-editor-row input,.form-editor-row select,.form-add-row input,.form-add-row select{height:30px;min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.2 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.form-window{border-color:#b9c1cd;background:#fdfdfd;box-shadow:0 12px 28px rgba(33,43,54,.12)}.form-window-title{justify-content:space-between;height:34px;background:linear-gradient(#f8f9fb,#e9edf3);border-bottom-color:#cbd3df;color:#1f2937}.form-window-title small{font-size:11px;color:#687385;font-weight:800}.form-command-bar{min-height:42px;background:#f4f6f9;border-bottom-color:#ccd4df}.form-command-bar button{height:27px;border-color:#aeb8c6;background:linear-gradient(#fff,#edf1f6);box-shadow:inset 0 1px 0 #fff;color:#1f2937}.form-command-bar button:hover{background:#fff}.form-fields{grid-template-columns:repeat(12,minmax(0,1fr));align-content:start;gap:8px 12px;padding:14px 16px 22px;background:#fbfbfc}.form-field{grid-column:1/-1;grid-template-columns:150px minmax(0,1fr);min-height:32px;padding:4px 6px;border:1px solid transparent}.form-field:hover{border-color:#b9c1cd;background:#fff}.form-field[data-html5-form-width="half"]{grid-column:span 6}.form-field[data-html5-form-width="third"]{grid-column:span 4}.form-field>label{font-size:12px;font-weight:800;color:#4b5563;align-self:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.form-input-control,.form-text-control{width:100%;border:1px solid #aeb8c6;background:#fff;padding:0 7px;color:#17202a;box-shadow:inset 0 1px 2px rgba(15,23,42,.06)}.form-input-control{height:28px}.form-text-control{min-height:64px;resize:none}.form-check-control{display:flex;align-items:center;gap:8px;color:#374151}.form-check-control input{width:16px;height:16px}.form-table-control{border:1px solid #aeb8c6;background:#fff;min-height:108px;overflow:hidden}.form-table-control div{display:grid;grid-template-columns:2fr 1fr 1fr}.form-table-control span{min-height:27px;border-right:1px solid #d7dde6;border-bottom:1px solid #d7dde6;padding:5px 7px;color:#374151;font-size:12px}.form-table-control div:first-child span{background:#eef2f7;font-weight:900}.form-group-control{min-height:44px;border:1px dashed #aeb8c6;background:#f6f8fb;padding:10px;color:#687385;font-weight:800}.form-editor-row{display:grid;gap:7px;padding:10px 12px;border-bottom:1px solid var(--line)}label.form-editor-row{grid-template-columns:88px minmax(0,1fr);align-items:center;text-transform:none}.form-editor-row small{color:var(--muted);font-size:11px}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr 118px 1fr 104px}.form-add-row{display:grid;grid-template-columns:minmax(0,1fr) 118px;gap:8px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.button.primary{background:var(--brand);border-color:var(--brand);color:#fff;margin:12px}.form-designer-foot small{color:var(--muted);font-weight:800}.form-window[data-html5-form-layout="compact"] .form-fields{gap:4px 8px}.form-window[data-html5-form-layout="compact"] .form-field{min-height:28px}.form-window[data-html5-form-layout="columns"] .form-field{grid-column:span 6}.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="table"],.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="group"]{grid-column:1/-1}
.form-empty-structure{grid-column:1/-1;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.form-empty-structure strong,.form-empty-structure span{display:block}.form-empty-structure strong{color:#1f2937}
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}} @media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
@media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}} @media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}}
@media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}} @media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}}