Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:31 +02:00
parent abf35d75f3
commit 50e0172f65
5 changed files with 27 additions and 73 deletions
+5 -3
View File
@@ -29,8 +29,10 @@ submissions, receipts, and handoff execution.
The module uses shared controls, dialogs, blockers, field help, statuses,
loading, empty/error states, disabled reasons, confirmations, and unsaved-draft
guards. The catalogue action row now uses the shared responsive
`ActionToolbar`; equal-column definition/editor groups use `FormGrid` and its
named narrow-layout collapse policy. These layout contracts do not change
guards. The catalogue now composes `WorkspaceFrame`, `ActionToolbar`,
`FilterBar`, and `StatePanel`; equal-column definition/editor groups use
`FormGrid`, while field, page, localization, and preview headings use the
shared section-header toolbar surface. Core therefore owns viewport, search,
state, heading, and narrow-layout geometry. These contracts do not change
filter scope, permissions, revision semantics, or publication consequences.
English and German catalogues cover the owned route and editor vocabulary.
+3 -2
View File
@@ -194,8 +194,9 @@ manifest = ModuleManifest(
summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.",
body=(
"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. The catalogue action row and "
"definition field groups reflow at narrow widths without changing filter scope, permissions, or lifecycle effects."
"Publishing is explicit; existing submissions continue to retain their exact revision. The catalogue, filter, empty states, "
"section headings, and definition field groups use the shared responsive layout language and reflow at narrow widths without "
"changing filter scope, permissions, or lifecycle effects."
),
layer="configured",
documentation_types=("admin", "user"),
@@ -1,6 +1,7 @@
import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { FormGrid,
ActionToolbar,
Button,
ConfirmDialog,
Dialog,
@@ -262,12 +263,12 @@ export default function FormDefinitionDialog({
</Field>
</FormGrid>
<div className="form-field-editor-heading">
<ActionToolbar surface="section-header" className="form-field-editor-heading">
<h3>Fields</h3>
<Button onClick={() => setDraft(addField(draft))} disabled={busy}>
<Plus size={16} aria-hidden="true" />Add field
</Button>
</div>
</ActionToolbar>
<div className="form-field-editor-list">
{draft.fields.map((field, index) =>
<div className="form-field-editor-row" key={`${index}:${field.key}`}>
@@ -417,12 +418,12 @@ function PageEditor({ draft, disabled, onChange }: { draft: FormDefinition; disa
}
return (
<section className="form-composition-section">
<div className="form-field-editor-heading">
<ActionToolbar surface="section-header" className="form-field-editor-heading">
<h3>Pages and sections</h3>
{pages.length === 0
? <Button disabled={disabled} onClick={() => updatePages([defaultPage(draft.fields)])}><Plus size={16} aria-hidden="true" />Enable pages</Button>
: <Button disabled={disabled} onClick={() => updatePages([...pages, emptyPage(pages.length + 1)])}><Plus size={16} aria-hidden="true" />Add page</Button>}
</div>
</ActionToolbar>
{pages.length === 0 && <p className="form-section-note">Fields render in their declared order on one page.</p>}
{pages.map((page, pageIndex) =>
<div className="form-page-editor" key={`${pageIndex}:${page.key}`}>
@@ -470,10 +471,10 @@ function LocalizationEditor({ draft, disabled, onChange }: { draft: FormDefiniti
}
return (
<section className="form-composition-section">
<div className="form-field-editor-heading">
<ActionToolbar surface="section-header" className="form-field-editor-heading">
<h3><Languages size={17} aria-hidden="true" />Localizations</h3>
<Button disabled={disabled} onClick={() => update([...localizations, emptyLocalization()])}><Plus size={16} aria-hidden="true" />Add locale</Button>
</div>
</ActionToolbar>
{localizations.length === 0 && <p className="form-section-note">The canonical labels are used for every locale.</p>}
{localizations.map((localization, index) =>
<div className="form-localization-editor" key={`${index}:${localization.locale}`}>
@@ -505,7 +506,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
const changed = previous ? definitionChanges(previous, definition) : ["New definition"];
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>
<ActionToolbar surface="section-header" className="form-field-editor-heading"><h3><Eye size={17} aria-hidden="true" />Preview and revision changes</h3></ActionToolbar>
<FormGrid columns={2} collapseAt="narrow" className="form-preview-grid">
<div>
<strong>{definition.title || "Untitled Form"}</strong>
+9 -6
View File
@@ -8,12 +8,15 @@ import { ActionToolbar,
DismissibleAlert,
IconButton,
FormField,
FilterBar,
LoadingIndicator,
PageScrollViewport,
StatePanel,
StatusBadge,
hasScope,
useUnsavedChanges,
useUnsavedDraftGuard,
WorkspaceFrame,
type PlatformRouteContext
} from "@govoplan/core-webui";
import {
@@ -144,13 +147,13 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
return (
<main className="forms-page">
<div className="forms-shell">
<ActionToolbar className="forms-toolbar">
<form onSubmit={search} className="forms-search">
<WorkspaceFrame className="forms-shell" label="Form definitions" interfaceId="forms.catalogue" helpContextId="forms.page.catalogue" helpModuleId="forms">
<ActionToolbar surface="panel-header" className="forms-toolbar">
<FilterBar as="form" surface="control" wrap="never" width="default" 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" />
<Button type="submit">Search</Button>
</form>
</FilterBar>
<label>
<span>State</span>
<select value={state} onChange={(event) => setState(event.target.value)}>
@@ -171,7 +174,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
{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} />}
{loading && <LoadingIndicator label="Loading Form definitions" />}
{!loading && !error && items.length === 0 && <div className="forms-empty">No matching definitions.</div>}
{!loading && !error && items.length === 0 && <StatePanel size="compact" description="No matching definitions." />}
{!loading && items.length > 0 &&
<div className="forms-list" role="list">
{items.map((item) => {
@@ -192,7 +195,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
</div>
}
</PageScrollViewport>
</div>
</WorkspaceFrame>
{editing &&
<FormDefinitionDialog
open
+2 -55
View File
@@ -1,39 +1,14 @@
.forms-page,
.forms-shell {
.forms-page {
height: 100%;
min-height: 0;
overflow: hidden;
}
.forms-shell {
display: flex;
flex-direction: column;
background: var(--surface);
}
.forms-toolbar {
display: flex;
align-items: center;
gap: 10px;
min-height: 58px;
padding: 10px 18px;
border-bottom: 1px solid var(--border);
background: var(--surface-raised);
}
.forms-search {
display: flex;
align-items: center;
gap: 8px;
width: min(520px, 100%);
flex: 1 1 520px;
margin-right: auto;
}
.forms-search input {
min-width: 120px;
flex: 1;
}
.forms-toolbar > label {
display: flex;
align-items: center;
@@ -122,12 +97,6 @@
color: var(--text-soft);
}
.forms-empty {
padding: 36px 0;
color: var(--text-soft);
text-align: center;
}
.form-definition-dialog {
width: min(1120px, calc(100vw - 32px));
height: min(860px, calc(100vh - 32px));
@@ -171,19 +140,6 @@
font-weight: 600;
}
.form-field-editor-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.form-field-editor-heading h3 {
margin: 0;
font-size: 0.98rem;
letter-spacing: 0;
}
.form-field-editor-list {
border-top: 1px solid var(--border);
}
@@ -313,15 +269,6 @@
}
@media (max-width: 900px) {
.forms-toolbar {
align-items: stretch;
flex-wrap: wrap;
}
.forms-search {
width: 100%;
}
.forms-row {
grid-template-columns: minmax(0, 1fr) auto auto;
}