Adopt shared WebUI layout primitives

This commit is contained in:
2026-08-18 10:42:52 +02:00
parent cf7da905a7
commit abf35d75f3
5 changed files with 23 additions and 38 deletions
@@ -1,6 +1,6 @@
import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import {
import { FormGrid,
Button,
ConfirmDialog,
Dialog,
@@ -198,7 +198,7 @@ export default function FormDefinitionDialog({
<div className="form-definition-editor">
<div className="form-definition-help"><DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} /></div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<div className="form-definition-grid">
<FormGrid columns={2} gap="small" collapseAt="narrow">
<Field label="Title">
<input value={draft.title} disabled={busy} onChange={(event) => setDraft({ ...draft, title: event.target.value })} />
</Field>
@@ -260,7 +260,7 @@ export default function FormDefinitionDialog({
placeholder="Optional instructions announced before the Form"
/>
</Field>
</div>
</FormGrid>
<div className="form-field-editor-heading">
<h3>Fields</h3>
@@ -333,19 +333,19 @@ export default function FormDefinitionDialog({
function ConstraintFields({ field, disabled, onChange }: { field: FormFieldDefinition; disabled: boolean; onChange: (value: Record<string, unknown>) => void }) {
if (["text", "multiline_text", "email"].includes(field.value_type)) {
return (
<div className="form-field-constraints">
<FormGrid columns={3} gap="compact" collapseAt="narrow" className="form-field-constraints">
<Field label="Minimum length"><input type="number" min={0} value={constraintValue(field.constraints.min_length)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "min_length", event.target.value))} /></Field>
<Field label="Maximum length"><input type="number" min={0} value={constraintValue(field.constraints.max_length)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "max_length", event.target.value))} /></Field>
<Field label="Pattern"><input value={String(field.constraints.pattern ?? "")} disabled={disabled} onChange={(event) => onChange(patchTextConstraint(field.constraints, "pattern", event.target.value))} /></Field>
</div>
</FormGrid>
);
}
if (["integer", "number"].includes(field.value_type)) {
return (
<div className="form-field-constraints">
<FormGrid columns={3} gap="compact" collapseAt="narrow" className="form-field-constraints">
<Field label="Minimum"><input type="number" value={constraintValue(field.constraints.minimum)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "minimum", event.target.value))} /></Field>
<Field label="Maximum"><input type="number" value={constraintValue(field.constraints.maximum)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "maximum", event.target.value))} /></Field>
</div>
</FormGrid>
);
}
return null;
@@ -367,7 +367,7 @@ function ConditionFields({
const predicate = condition?.kind === "predicate" ? condition : null;
const candidates = fields.filter((item) => item.key !== currentKey && item.key.trim());
return (
<div className="form-field-condition">
<FormGrid columns={3} gap="compact" collapseAt="narrow" className="form-field-condition">
<Field label="Visible when">
<select
value={predicate?.field_key ?? ""}
@@ -406,7 +406,7 @@ function ConditionFields({
</Field>
}
</>}
</div>
</FormGrid>
);
}
@@ -506,7 +506,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
return (
<section className="form-composition-section form-definition-preview">
<div className="form-field-editor-heading"><h3><Eye size={17} aria-hidden="true" />Preview and revision changes</h3></div>
<div className="form-preview-grid">
<FormGrid columns={2} collapseAt="narrow" className="form-preview-grid">
<div>
<strong>{definition.title || "Untitled Form"}</strong>
{(definition.pages?.length ? definition.pages : [defaultPage(definition.fields)]).map((page) =>
@@ -517,7 +517,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
)}
</div>
<div><strong>Changes</strong><ul>{changed.map((item) => <li key={item}>{item}</li>)}</ul></div>
</div>
</FormGrid>
</section>
);
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { Download, Pencil, Plus, RefreshCw, Search, Upload } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
import {
import { ActionToolbar,
ActionBlockerHint,
Button,
Dialog,
@@ -145,7 +145,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
return (
<main className="forms-page">
<div className="forms-shell">
<div className="forms-toolbar">
<ActionToolbar className="forms-toolbar">
<form onSubmit={search} className="forms-search">
<Search size={17} aria-hidden="true" />
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search definitions" aria-label="Search Form definitions" />
@@ -166,7 +166,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
<Button variant="primary" disabled={!canWrite} disabledReason={!canWrite ? FORMS_I18N.writeReason : undefined} onClick={() => setEditing("new")}><Plus size={16} aria-hidden="true" />New definition</Button>
<span className="forms-count">{total}</span>
<DocumentationHelpLink reference={FORMS_DOCUMENTATION} />
</div>
</ActionToolbar>
<PageScrollViewport className="forms-list-viewport">
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{!canWrite && <ActionBlockerHint tone="info" reason={{ summary: "No Form management permission", details: FORMS_I18N.writeReason, requiredAction: FORMS_I18N.permissionAction, actor: FORMS_I18N.permissionActor, target: FORMS_I18N.permissionDestination }} labels={{ requiredAction: FORMS_I18N.requiredAction, actor: FORMS_I18N.actor, target: FORMS_I18N.destination }} documentation={FORMS_DOCUMENTATION} />}
+2 -21
View File
@@ -145,12 +145,6 @@
justify-content: flex-end;
}
.form-definition-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 16px;
}
.form-definition-wide {
grid-column: 1 / -1;
}
@@ -229,13 +223,6 @@
grid-column: 2 / -1;
}
.form-field-constraints,
.form-field-condition {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 9px;
}
.form-composition-section {
display: flex;
flex-direction: column;
@@ -302,9 +289,7 @@
}
.form-preview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
min-width: 0;
}
.form-preview-grid > div {
@@ -374,14 +359,10 @@
}
@media (max-width: 620px) {
.form-definition-grid,
.form-field-constraints,
.form-field-condition,
.form-page-editor-heading,
.form-section-editor,
.form-localization-editor,
.form-localization-fields,
.form-preview-grid {
.form-localization-fields {
grid-template-columns: 1fr;
}