Enforce shared WebUI primitive adoption
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m55s

This commit is contained in:
2026-08-18 10:42:55 +02:00
parent 5e9234d4b6
commit 5efb0eea6f
4 changed files with 267 additions and 4 deletions
@@ -102,10 +102,31 @@ Migration is incremental and enforceable:
5. promote the next repeated structure only after its variants and extension
points are understood.
The intended next structural contracts are responsive page toolbar, content
grid, form-section layout, dialog body/footer layout and shared empty/error
state composition. Their APIs must remain composable; a central component is
not a single oversized page template.
The current structural vocabulary is:
- `ActionToolbar`, `ToolbarGroup`, and `ToolbarSpacer` own action alignment,
density, grouping, accessible toolbar naming, help identity, and responsive
wrapping. Modules may add surface padding or domain-specific presentation;
they do not recreate the flex/wrap skeleton.
- `ContentGrid`, `FormGrid`, `FormLayout`, and `GridItem` own equal-column
geometry, standard gaps, alignment, spans, native form semantics, and named
responsive collapse points. A module-local grid remains appropriate only
when unequal tracks or domain visualization semantics are material.
- `FormSection` owns form-section heading, description, actions, content flow,
separation, and panel presentation. It does not own field values,
validation, permissions, or domain wording.
- `Dialog` owns size and administration variants, body padding, description,
notices, and fixed footer placement. `DialogActions`, `DialogForm`, and
`DialogSection` own the footer action flow, native form flow, and body
grouping used inside it. Modules compose fields and consequences rather than
recreating dialog anatomy.
Raw toolbar tags, the former generic grid classes, raw dialog-form wrappers,
and module-local definitions of these contracts are rejected by the focused
workspace checks. The next promotion candidates are metric groups,
description/property lists, filter/search composition, responsive data
summaries, and unified empty/error state composition. Their APIs must remain
composable; a central component is not a single oversized page template.
## Surface Archetypes
@@ -0,0 +1,52 @@
# Shared WebUI Primitive Inventory
This 2026-08-18 inventory records the implementation state after the first
product-wide toolbar, grid, form-section, and dialog-anatomy migration. It is
evidence for prioritization, not a substitute for the normative
[interface pattern language](../architecture/INTERFACE_PATTERN_LANGUAGE.md).
## Implemented And Enforced
| Contract | Adoption evidence | Ownership now enforced |
| --- | ---: | --- |
| `ActionToolbar` and groups | 39 source files | Raw module-prefixed toolbar elements and local toolbar component definitions are rejected. |
| `ContentGrid` | 15 source files | Former `dashboard-grid` and `settings-grid` wrappers are rejected. |
| `FormGrid` and `FormLayout` | 53 source files | Former `form-grid` and `admin-form-grid` wrappers are rejected; equal-column dialog and editor grids were migrated even when they had module-prefixed names. |
| `FormSection` | 2 representative source files | The reusable section API, variants, responsive action placement, and heading anatomy are Core-owned; broader adoption is incremental. |
| `DialogActions` | Every Core `Dialog` footer | Footer wrapping and alignment are no longer repeated by consumers. |
| `DialogForm` | 6 source files | Raw `*-dialog-form` form wrappers are rejected. |
| `DialogSection` | 6 source files | Repeated dialog field/content grouping is available as a shared primitive. |
`tools/checks/check-shared-webui-primitives.py` verifies Core exports and
ownership, representative consumers, the absence of the retired raw anatomy,
and composition of every `Dialog` footer through `DialogActions`.
## Remaining Promotion Candidates
The post-migration scan found 101 uses of 30 non-Core grid class names across
54 files. Most are intentionally domain-specific; repeated groups identify the
next useful primitives:
| Priority | Remaining pattern | Evidence | Proposed contract |
| --- | --- | ---: | --- |
| 1 | Metric-card groups | `metric-grid`: 29 uses in 25 files | `MetricGrid` or `MetricGroup` owning 14/auto-fit columns, compact spacing, inset mode, and narrow stacking around the existing `MetricCard`. |
| 2 | Property/description lists | `admin-details-grid`: 13 uses in 11 files | Semantic `DescriptionList` with compact/default density, 12 columns, wrapping, and narrow collapse. |
| 3 | Assignment/picker groups | `admin-assignment-grid`: 5 uses in 4 files | A selection/assignment layout only after its list, search, empty, and permission variants are compared; plain geometry can already use `ContentGrid`. |
| 4 | Filter and search rows | Toolbars are centralized, but filter fields, result counts, bulk selection, and action grouping still vary | `FilterBar` composed from `ActionToolbar`, with an explicit search region, filters, result summary, and bulk actions. |
| 5 | Dialog sizing exceptions | Dialog consumers still use 62 module class tokens across 53 files; 26 module/Core CSS files contain dialog/modal width declarations needing review | Migrate standard widths to `Dialog size`; retain only documented full-canvas, preview, chooser, and domain-editor exceptions. |
| 6 | State and collection anatomy | Loading components exist, but empty/error/retry/action placement is not yet one composition | A composable `StatePanel`/`CollectionState` covering empty, recoverable error, permission block, partial result, and next action without hiding domain consequences. |
The other custom grids are mostly bounded domain visualizations or unequal-track
editors: conflict mappings, import mapping, campaign review facts, charts,
calendar time views, connector synchronization, records lists, and definition
editors. They should remain module-owned unless a second domain demonstrates
the same semantics and interaction contract. A shared primitive should not be
created merely because two implementations both use CSS Grid.
## Next Audit
The next bounded slice should implement the metric and description-list
contracts, migrate their consumers, and then classify dialog width declarations
as standard-size migrations or registered exceptions. After that, compare
filter/search rows before defining their composition; their accessibility and
bulk-selection behavior matter more than visual similarity.
+2
View File
@@ -90,6 +90,7 @@ PY
"$PYTHON" -c 'import govoplan_core.db.bootstrap; import govoplan_access.backend.admin.service; import govoplan_addresses.backend.manifest; import govoplan_files.backend.router; import govoplan_mail.backend.sending.imap; print("targeted backend imports passed")'
"$META_ROOT/tools/checks/check_dependency_boundaries.py"
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-layouts.py"
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-primitives.py"
"$PYTHON" -m unittest tests.test_module_system
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
@@ -116,6 +117,7 @@ PY
"$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count
cd "$ROOT/webui"
"$NPM" run test:layout-primitives
"$NPM" run test:mail-components
"$NPM" run test:module-capabilities
"$NPM" run test:module-permutations
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Enforce Core ownership of repeated WebUI toolbar, grid and dialog anatomy."""
from __future__ import annotations
import pathlib
import re
import sys
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPOS_ROOT = META_ROOT.parent
CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts")
RAW_ELEMENT = re.compile(
r'<(?P<tag>div|span|header|section|form)\b(?P<attrs>[^>]*?)'
r'\bclassName="(?P<classes>[^"]+)"',
re.DOTALL,
)
LEGACY_LAYOUT_TOKENS = {
"form-grid",
"admin-form-grid",
"dashboard-grid",
"settings-grid",
"admin-dialog",
"admin-dialog-wide",
}
CENTRAL_COMPONENTS = {
"ActionToolbar": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ToolbarGroup": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ToolbarSpacer": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ContentGrid": pathlib.Path(
"govoplan-core/webui/src/components/ContentGrid.tsx"
),
"FormGrid": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
"FormLayout": pathlib.Path(
"govoplan-core/webui/src/components/ContentGrid.tsx"
),
"GridItem": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
"FormSection": pathlib.Path(
"govoplan-core/webui/src/components/FormSection.tsx"
),
"DialogActions": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
"DialogForm": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
"DialogSection": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
}
REQUIRED_CONSUMERS = {
"ActionToolbar": (
pathlib.Path("govoplan-core/webui/src/components/WysiwygEditor.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarPage.tsx"),
pathlib.Path("govoplan-files/webui/src/features/files/FilesPage.tsx"),
pathlib.Path("govoplan-forms/webui/src/features/forms/FormsPage.tsx"),
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
),
"ContentGrid": (
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSettingsPanel.tsx"),
),
"FormGrid": (
pathlib.Path("govoplan-core/webui/src/components/mail/MailServerSettingsPanel.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
pathlib.Path("govoplan-forms/webui/src/features/forms/FormDefinitionDialog.tsx"),
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxAdminPanel.tsx"),
),
"FormSection": (
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
pathlib.Path("govoplan-quick-access/webui/src/features/settings/QuickAccessSettingsPanel.tsx"),
),
"DialogForm": (
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
),
"DialogSection": (
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"),
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
),
}
def source_paths() -> list[pathlib.Path]:
paths: list[pathlib.Path] = []
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
source_root = repository / "webui" / "src"
if source_root.is_dir():
paths.extend(sorted(source_root.rglob("*.tsx")))
return paths
def relative(path: pathlib.Path) -> pathlib.Path:
return path.relative_to(REPOS_ROOT)
def raw_reason(classes: str) -> str | None:
tokens = classes.split()
legacy = sorted(set(tokens) & LEGACY_LAYOUT_TOKENS)
if legacy:
return f"legacy shared layout class {', '.join(legacy)}"
toolbars = [
token
for token in tokens
if token == "admin-toolbar-row" or token.endswith("-toolbar")
]
if toolbars:
return f"raw toolbar class {', '.join(toolbars)}"
dialog_forms = [token for token in tokens if token.endswith("dialog-form")]
if dialog_forms:
return f"raw dialog form class {', '.join(dialog_forms)}"
return None
def main() -> int:
sources = source_paths()
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
errors: list[str] = []
for path, content in source_text.items():
for match in RAW_ELEMENT.finditer(content):
reason = raw_reason(match.group("classes"))
if reason is None:
continue
line = content.count("\n", 0, match.start()) + 1
errors.append(
f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}"
)
for name, owner in CENTRAL_COMPONENTS.items():
definition = re.compile(
rf"\b(?:function|class)\s+{name}\b|\bconst\s+{name}\s*="
)
for path, content in source_text.items():
if path != owner and definition.search(content):
errors.append(f"Module-local {name} definition is not allowed: {path}")
usage_counts: dict[str, int] = {}
for name in CENTRAL_COMPONENTS:
usage = re.compile(rf"<{name}\b")
usage_counts[name] = sum(bool(usage.search(content)) for content in source_text.values())
for name, consumers in REQUIRED_CONSUMERS.items():
for path in consumers:
absolute = REPOS_ROOT / path
if not absolute.exists():
continue
if f"<{name}" not in absolute.read_text(encoding="utf-8"):
errors.append(f"Required shared {name} consumer regressed: {path}")
core_index_path = REPOS_ROOT / CORE_INDEX
if core_index_path.exists():
core_index = core_index_path.read_text(encoding="utf-8")
for name in CENTRAL_COMPONENTS:
if not re.search(rf"\b{name}\b", core_index):
errors.append(f"Core must export {name} from @govoplan/core-webui.")
dialog_path = REPOS_ROOT / "govoplan-core/webui/src/components/Dialog.tsx"
if dialog_path.exists():
dialog_text = dialog_path.read_text(encoding="utf-8")
if "<DialogActions" not in dialog_text:
errors.append("Every Core Dialog footer must compose DialogActions.")
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
migrated = ", ".join(
f"{name}={usage_counts[name]} files"
for name in ("ActionToolbar", "ContentGrid", "FormGrid", "FormSection", "DialogForm", "DialogSection")
)
print(f"Shared WebUI primitive contract passed: {migrated}; no raw legacy anatomy.")
return 0
if __name__ == "__main__":
raise SystemExit(main())