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
+5 -2
View File
@@ -29,5 +29,8 @@ submissions, receipts, and handoff execution.
The module uses shared controls, dialogs, blockers, field help, statuses, The module uses shared controls, dialogs, blockers, field help, statuses,
loading, empty/error states, disabled reasons, confirmations, and unsaved-draft loading, empty/error states, disabled reasons, confirmations, and unsaved-draft
guards. Existing responsive list/editor layouts remain bounded. English and guards. The catalogue action row now uses the shared responsive
German catalogues cover the owned route and editor vocabulary. `ActionToolbar`; equal-column definition/editor groups use `FormGrid` and its
named narrow-layout collapse policy. These layout contracts do not change
filter scope, permissions, revision semantics, or publication consequences.
English and German catalogues cover the owned route and editor vocabulary.
+2 -1
View File
@@ -194,7 +194,8 @@ manifest = ModuleManifest(
summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.", summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.",
body=( body=(
"Each revision fixes field types, options, constraints, draft, attachment, signature, policy, and handoff requirements. " "Each revision fixes field types, options, constraints, draft, attachment, signature, policy, and handoff requirements. "
"Publishing is explicit; existing submissions continue to retain their exact revision." "Publishing is explicit; existing submissions continue to retain their exact revision. The catalogue action row and "
"definition field groups reflow at narrow widths without changing filter scope, permissions, or lifecycle effects."
), ),
layer="configured", layer="configured",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
@@ -1,6 +1,6 @@
import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react"; import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { import { FormGrid,
Button, Button,
ConfirmDialog, ConfirmDialog,
Dialog, Dialog,
@@ -198,7 +198,7 @@ export default function FormDefinitionDialog({
<div className="form-definition-editor"> <div className="form-definition-editor">
<div className="form-definition-help"><DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} /></div> <div className="form-definition-help"><DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} /></div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>} {error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<div className="form-definition-grid"> <FormGrid columns={2} gap="small" collapseAt="narrow">
<Field label="Title"> <Field label="Title">
<input value={draft.title} disabled={busy} onChange={(event) => setDraft({ ...draft, title: event.target.value })} /> <input value={draft.title} disabled={busy} onChange={(event) => setDraft({ ...draft, title: event.target.value })} />
</Field> </Field>
@@ -260,7 +260,7 @@ export default function FormDefinitionDialog({
placeholder="Optional instructions announced before the Form" placeholder="Optional instructions announced before the Form"
/> />
</Field> </Field>
</div> </FormGrid>
<div className="form-field-editor-heading"> <div className="form-field-editor-heading">
<h3>Fields</h3> <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 }) { function ConstraintFields({ field, disabled, onChange }: { field: FormFieldDefinition; disabled: boolean; onChange: (value: Record<string, unknown>) => void }) {
if (["text", "multiline_text", "email"].includes(field.value_type)) { if (["text", "multiline_text", "email"].includes(field.value_type)) {
return ( 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="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="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> <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)) { if (["integer", "number"].includes(field.value_type)) {
return ( 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="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> <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; return null;
@@ -367,7 +367,7 @@ function ConditionFields({
const predicate = condition?.kind === "predicate" ? condition : null; const predicate = condition?.kind === "predicate" ? condition : null;
const candidates = fields.filter((item) => item.key !== currentKey && item.key.trim()); const candidates = fields.filter((item) => item.key !== currentKey && item.key.trim());
return ( return (
<div className="form-field-condition"> <FormGrid columns={3} gap="compact" collapseAt="narrow" className="form-field-condition">
<Field label="Visible when"> <Field label="Visible when">
<select <select
value={predicate?.field_key ?? ""} value={predicate?.field_key ?? ""}
@@ -406,7 +406,7 @@ function ConditionFields({
</Field> </Field>
} }
</>} </>}
</div> </FormGrid>
); );
} }
@@ -506,7 +506,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
return ( return (
<section className="form-composition-section form-definition-preview"> <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-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> <div>
<strong>{definition.title || "Untitled Form"}</strong> <strong>{definition.title || "Untitled Form"}</strong>
{(definition.pages?.length ? definition.pages : [defaultPage(definition.fields)]).map((page) => {(definition.pages?.length ? definition.pages : [defaultPage(definition.fields)]).map((page) =>
@@ -517,7 +517,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
)} )}
</div> </div>
<div><strong>Changes</strong><ul>{changed.map((item) => <li key={item}>{item}</li>)}</ul></div> <div><strong>Changes</strong><ul>{changed.map((item) => <li key={item}>{item}</li>)}</ul></div>
</div> </FormGrid>
</section> </section>
); );
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import { Download, Pencil, Plus, RefreshCw, Search, Upload } from "lucide-react"; import { Download, Pencil, Plus, RefreshCw, Search, Upload } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react"; import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
import { import { ActionToolbar,
ActionBlockerHint, ActionBlockerHint,
Button, Button,
Dialog, Dialog,
@@ -145,7 +145,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
return ( return (
<main className="forms-page"> <main className="forms-page">
<div className="forms-shell"> <div className="forms-shell">
<div className="forms-toolbar"> <ActionToolbar className="forms-toolbar">
<form onSubmit={search} className="forms-search"> <form onSubmit={search} className="forms-search">
<Search size={17} aria-hidden="true" /> <Search size={17} aria-hidden="true" />
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search definitions" aria-label="Search Form definitions" /> <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> <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> <span className="forms-count">{total}</span>
<DocumentationHelpLink reference={FORMS_DOCUMENTATION} /> <DocumentationHelpLink reference={FORMS_DOCUMENTATION} />
</div> </ActionToolbar>
<PageScrollViewport className="forms-list-viewport"> <PageScrollViewport className="forms-list-viewport">
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>} {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} />} {!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; justify-content: flex-end;
} }
.form-definition-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 16px;
}
.form-definition-wide { .form-definition-wide {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
@@ -229,13 +223,6 @@
grid-column: 2 / -1; 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 { .form-composition-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -302,9 +289,7 @@
} }
.form-preview-grid { .form-preview-grid {
display: grid; min-width: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
} }
.form-preview-grid > div { .form-preview-grid > div {
@@ -374,14 +359,10 @@
} }
@media (max-width: 620px) { @media (max-width: 620px) {
.form-definition-grid,
.form-field-constraints,
.form-field-condition,
.form-page-editor-heading, .form-page-editor-heading,
.form-section-editor, .form-section-editor,
.form-localization-editor, .form-localization-editor,
.form-localization-fields, .form-localization-fields {
.form-preview-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }