chore: enforce shared workspace layouts
Dependency Audit / dependency-audit (push) Successful in 1m35s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m19s

This commit is contained in:
2026-08-18 02:17:24 +02:00
parent b76581a89a
commit 5e9234d4b6
5 changed files with 99 additions and 31 deletions
+18 -11
View File
@@ -71,13 +71,20 @@ entity, permission, workflow state, endpoint, or policy decision. Reuse does
not justify moving domain semantics into Core. not justify moving domain semantics into Core.
`PageLayout` is the standard frame for headed workflow, dashboard, `PageLayout` is the standard frame for headed workflow, dashboard,
configuration, monitoring and explanatory pages. It owns the scroll viewport, configuration, monitoring and explanatory pages. It owns the content inset,
content inset, sticky responsive header, title and description geometry, sticky responsive header, title and rich-description geometry, route-action
route-action placement, transient page notices, loading boundary and page help placement, transient and custom notices, loading boundary and page help
identity. `PageHeader` is the escape hatch for full-canvas archetypes that need identity. Its modes make scroll ownership explicit: `standalone` owns a page
the same header contract but must own their workspace scroll. Specialized viewport, `workspace` defers scrolling to a full-canvas content pane while
layouts such as `AdminPageLayout` compose these lower-level Core contracts; retaining the standard inset, and `embedded` owns neither scroll nor inset.
they do not repeat their markup or responsive CSS.
`WorkspaceLayout` is the standard full-canvas shell. Its `navigation` variant
owns module/resource subnavigation plus content; its `split` variant owns
collection/detail panes. It centralizes pane sizing, internal scroll,
responsive collapse/stacking, accessible pane labels and workspace help
identity. `PageHeader` remains available when an exceptional canvas needs only
the shared heading. Specialized layouts such as `AdminPageLayout` compose
these lower-level Core contracts; they do not repeat markup or responsive CSS.
Module CSS may arrange domain content inside a shared layout. It must not Module CSS may arrange domain content inside a shared layout. It must not
override Core layout internals or copy the outer page, dialog, toolbar, form or override Core layout internals or copy the outer page, dialog, toolbar, form or
@@ -95,10 +102,10 @@ Migration is incremental and enforceable:
5. promote the next repeated structure only after its variants and extension 5. promote the next repeated structure only after its variants and extension
points are understood. points are understood.
The intended next structural contracts are workspace/split-pane layout, The intended next structural contracts are responsive page toolbar, content
responsive page toolbar, content grid, form-section layout, dialog body/footer grid, form-section layout, dialog body/footer layout and shared empty/error
layout and shared empty/error state composition. Their APIs must remain state composition. Their APIs must remain composable; a central component is
composable; a central component is not a single oversized page template. not a single oversized page template.
## Surface Archetypes ## Surface Archetypes
+4
View File
@@ -132,6 +132,9 @@ cd /mnt/DATA/git/govoplan-workflow/webui
cd /mnt/DATA/git/govoplan-dashboard/webui cd /mnt/DATA/git/govoplan-dashboard/webui
"$NPM" run test:dashboard-layout "$NPM" run test:dashboard-layout
cd /mnt/DATA/git/govoplan-approvals/webui
"$NPM" run test:workspace-layout
cd /mnt/DATA/git/govoplan-postbox/webui cd /mnt/DATA/git/govoplan-postbox/webui
"$NPM" run test:ui-structure "$NPM" run test:ui-structure
@@ -141,3 +144,4 @@ cd /mnt/DATA/git/govoplan-mail/webui
cd /mnt/DATA/git/govoplan-campaign/webui cd /mnt/DATA/git/govoplan-campaign/webui
"$NPM" run test:policy-ui "$NPM" run test:policy-ui
"$NPM" run test:template-preview "$NPM" run test:template-preview
"$NPM" run test:accessibility-contract
+71 -4
View File
@@ -11,21 +11,53 @@ import sys
META_ROOT = pathlib.Path(__file__).resolve().parents[2] META_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPOS_ROOT = META_ROOT.parent REPOS_ROOT = META_ROOT.parent
BASELINE_PATH = pathlib.Path(__file__).with_name("shared-webui-layout-baseline.txt") BASELINE_PATH = pathlib.Path(__file__).with_name("shared-webui-layout-baseline.txt")
WORKSPACE_BASELINE_PATH = pathlib.Path(__file__).with_name(
"shared-webui-workspace-baseline.txt"
)
RAW_PAGE_FRAME = 'className="content-pad workspace-data-page' RAW_PAGE_FRAME = 'className="content-pad workspace-data-page'
RAW_WORKSPACE = re.compile(r'<div\s+className="workspace(?:\s|\")')
LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b") LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b")
LOCAL_WORKSPACE_LAYOUT = re.compile(
r"\b(?:function|class|const)\s+WorkspaceLayout\b"
)
CENTRAL_LAYOUT = pathlib.Path("govoplan-core/webui/src/components/PageLayout.tsx") CENTRAL_LAYOUT = pathlib.Path("govoplan-core/webui/src/components/PageLayout.tsx")
CENTRAL_WORKSPACE_LAYOUT = pathlib.Path(
"govoplan-core/webui/src/components/WorkspaceLayout.tsx"
)
REQUIRED_CONSUMERS = ( REQUIRED_CONSUMERS = (
pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx"), pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx"),
pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"), pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"),
pathlib.Path("govoplan-dashboard/webui/src/features/dashboard/DashboardPage.tsx"), pathlib.Path("govoplan-dashboard/webui/src/features/dashboard/DashboardPage.tsx"),
pathlib.Path("govoplan-ops/webui/src/features/ops/OpsPage.tsx"), pathlib.Path("govoplan-ops/webui/src/features/ops/OpsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/AttachmentsDataPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignAuditPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignFieldsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignJsonView.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignListPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/MailSettingsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/TemplateDataPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/reports/AggregateReportsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/templates/TemplatesPage.tsx"),
)
REQUIRED_WORKSPACE_CONSUMERS = (
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignWorkspace.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignModulePage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/templates/TemplatesPage.tsx"),
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
) )
def baseline_paths() -> set[pathlib.Path]: def baseline_paths(path: pathlib.Path) -> set[pathlib.Path]:
return { return {
pathlib.Path(line.strip()) pathlib.Path(line.strip())
for line in BASELINE_PATH.read_text(encoding="utf-8").splitlines() for line in path.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#") if line.strip() and not line.lstrip().startswith("#")
} }
@@ -47,7 +79,7 @@ def main() -> int:
sources = source_paths() sources = source_paths()
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources} source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text} raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text}
baseline = baseline_paths() baseline = baseline_paths(BASELINE_PATH)
available_baseline = { available_baseline = {
path for path in baseline if (REPOS_ROOT / path.parts[0]).is_dir() path for path in baseline if (REPOS_ROOT / path.parts[0]).is_dir()
} }
@@ -63,9 +95,30 @@ def main() -> int:
errors.append("Remove migrated page frames from the shared-layout baseline:") errors.append("Remove migrated page frames from the shared-layout baseline:")
errors.extend(f"- {path}" for path in resolved) errors.extend(f"- {path}" for path in resolved)
raw_workspaces = {
path for path, text in source_text.items() if RAW_WORKSPACE.search(text)
}
workspace_baseline = baseline_paths(WORKSPACE_BASELINE_PATH)
available_workspace_baseline = {
path
for path in workspace_baseline
if (REPOS_ROOT / path.parts[0]).is_dir()
}
unexpected_workspaces = sorted(raw_workspaces - available_workspace_baseline)
if unexpected_workspaces:
errors.append("New raw workspaces must use @govoplan/core-webui WorkspaceLayout:")
errors.extend(f"- {path}" for path in unexpected_workspaces)
resolved_workspaces = sorted(available_workspace_baseline - raw_workspaces)
if resolved_workspaces:
errors.append("Remove migrated workspaces from the shared-workspace baseline:")
errors.extend(f"- {path}" for path in resolved_workspaces)
for path, text in source_text.items(): for path, text in source_text.items():
if path != CENTRAL_LAYOUT and LOCAL_PAGE_LAYOUT.search(text): if path != CENTRAL_LAYOUT and LOCAL_PAGE_LAYOUT.search(text):
errors.append(f"Module-local PageLayout definition is not allowed: {path}") errors.append(f"Module-local PageLayout definition is not allowed: {path}")
if path != CENTRAL_WORKSPACE_LAYOUT and LOCAL_WORKSPACE_LAYOUT.search(text):
errors.append(f"Module-local WorkspaceLayout definition is not allowed: {path}")
for path in REQUIRED_CONSUMERS: for path in REQUIRED_CONSUMERS:
absolute_path = REPOS_ROOT / path absolute_path = REPOS_ROOT / path
@@ -77,9 +130,21 @@ def main() -> int:
if RAW_PAGE_FRAME in text: if RAW_PAGE_FRAME in text:
errors.append(f"Required shared-layout consumer restored the raw page frame: {path}") errors.append(f"Required shared-layout consumer restored the raw page frame: {path}")
for path in REQUIRED_WORKSPACE_CONSUMERS:
absolute_path = REPOS_ROOT / path
if not absolute_path.exists():
continue
text = absolute_path.read_text(encoding="utf-8")
if "<WorkspaceLayout" not in text:
errors.append(f"Required shared-workspace consumer no longer uses WorkspaceLayout: {path}")
if RAW_WORKSPACE.search(text):
errors.append(f"Required shared-workspace consumer restored the raw workspace: {path}")
core_index = REPOS_ROOT / "govoplan-core/webui/src/index.ts" core_index = REPOS_ROOT / "govoplan-core/webui/src/index.ts"
if core_index.exists() and "PageLayout, PageHeader" not in core_index.read_text(encoding="utf-8"): if core_index.exists() and "PageLayout, PageHeader" not in core_index.read_text(encoding="utf-8"):
errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.") errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.")
if core_index.exists() and "WorkspaceLayout" not in core_index.read_text(encoding="utf-8"):
errors.append("Core must export WorkspaceLayout from @govoplan/core-webui.")
if errors: if errors:
print("\n".join(errors), file=sys.stderr) print("\n".join(errors), file=sys.stderr)
@@ -88,7 +153,9 @@ def main() -> int:
print( print(
"Shared WebUI layout contract passed: " "Shared WebUI layout contract passed: "
f"{len(REQUIRED_CONSUMERS)} central consumers, " f"{len(REQUIRED_CONSUMERS)} central consumers, "
f"{len(raw_frames)} registered legacy page-frame files." f"{len(raw_frames)} registered legacy page-frame files; "
f"{len(REQUIRED_WORKSPACE_CONSUMERS)} workspace consumers, "
f"{len(raw_workspaces)} registered legacy workspace files."
) )
return 0 return 0
@@ -1,22 +1,6 @@
# Existing raw page frames. Remove an entry when that surface adopts the Core # Existing raw page frames. Remove an entry when that surface adopts the Core
# PageLayout contract. New entries are rejected by check-shared-webui-layouts.py. # PageLayout contract. New entries are rejected by check-shared-webui-layouts.py.
govoplan-access/webui/src/features/admin/AdminPage.tsx govoplan-access/webui/src/features/admin/AdminPage.tsx
govoplan-campaign/webui/src/features/campaigns/AttachmentsDataPage.tsx
govoplan-campaign/webui/src/features/campaigns/CampaignAuditPage.tsx
govoplan-campaign/webui/src/features/campaigns/CampaignFieldsPage.tsx
govoplan-campaign/webui/src/features/campaigns/CampaignJsonView.tsx
govoplan-campaign/webui/src/features/campaigns/CampaignListPage.tsx
govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx
govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx
govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx
govoplan-campaign/webui/src/features/campaigns/MailSettingsPage.tsx
govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx
govoplan-campaign/webui/src/features/campaigns/TemplateDataPage.tsx
govoplan-campaign/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
govoplan-campaign/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx
govoplan-campaign/webui/src/features/reports/AggregateReportsPage.tsx
govoplan-campaign/webui/src/features/templates/TemplatesPage.tsx
govoplan-core/webui/src/features/settings/SettingsPage.tsx govoplan-core/webui/src/features/settings/SettingsPage.tsx
govoplan-docs/webui/src/features/docs/DocsPage.tsx govoplan-docs/webui/src/features/docs/DocsPage.tsx
govoplan-mail/webui/src/features/mail/MailBouncePage.tsx govoplan-mail/webui/src/features/mail/MailBouncePage.tsx
@@ -0,0 +1,6 @@
# Existing raw full-canvas workspaces. Remove an entry when that surface adopts
# WorkspaceLayout. New entries are rejected by check-shared-webui-layouts.py.
govoplan-access/webui/src/features/admin/AdminPage.tsx
govoplan-core/webui/src/features/settings/SettingsPage.tsx
govoplan-docs/webui/src/features/docs/DocsPage.tsx
govoplan-organizations/webui/src/features/organizations/OrganizationsPage.tsx