diff --git a/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md b/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md
index edcb341..5aa92d8 100644
--- a/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md
+++ b/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md
@@ -71,13 +71,20 @@ entity, permission, workflow state, endpoint, or policy decision. Reuse does
not justify moving domain semantics into Core.
`PageLayout` is the standard frame for headed workflow, dashboard,
-configuration, monitoring and explanatory pages. It owns the scroll viewport,
-content inset, sticky responsive header, title and description geometry,
-route-action placement, transient page notices, loading boundary and page help
-identity. `PageHeader` is the escape hatch for full-canvas archetypes that need
-the same header contract but must own their workspace scroll. Specialized
-layouts such as `AdminPageLayout` compose these lower-level Core contracts;
-they do not repeat their markup or responsive CSS.
+configuration, monitoring and explanatory pages. It owns the content inset,
+sticky responsive header, title and rich-description geometry, route-action
+placement, transient and custom notices, loading boundary and page help
+identity. Its modes make scroll ownership explicit: `standalone` owns a page
+viewport, `workspace` defers scrolling to a full-canvas content pane while
+retaining the standard inset, and `embedded` owns neither scroll nor inset.
+
+`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
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
points are understood.
-The intended next structural contracts are workspace/split-pane layout,
-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 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.
## Surface Archetypes
diff --git a/tools/checks/check-focused.sh b/tools/checks/check-focused.sh
index b6eb423..4eef100 100644
--- a/tools/checks/check-focused.sh
+++ b/tools/checks/check-focused.sh
@@ -132,6 +132,9 @@ cd /mnt/DATA/git/govoplan-workflow/webui
cd /mnt/DATA/git/govoplan-dashboard/webui
"$NPM" run test:dashboard-layout
+cd /mnt/DATA/git/govoplan-approvals/webui
+"$NPM" run test:workspace-layout
+
cd /mnt/DATA/git/govoplan-postbox/webui
"$NPM" run test:ui-structure
@@ -141,3 +144,4 @@ cd /mnt/DATA/git/govoplan-mail/webui
cd /mnt/DATA/git/govoplan-campaign/webui
"$NPM" run test:policy-ui
"$NPM" run test:template-preview
+"$NPM" run test:accessibility-contract
diff --git a/tools/checks/check-shared-webui-layouts.py b/tools/checks/check-shared-webui-layouts.py
index b8b8758..077813d 100644
--- a/tools/checks/check-shared-webui-layouts.py
+++ b/tools/checks/check-shared-webui-layouts.py
@@ -11,21 +11,53 @@ import sys
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPOS_ROOT = META_ROOT.parent
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_WORKSPACE = re.compile(r'
set[pathlib.Path]:
+def baseline_paths(path: pathlib.Path) -> set[pathlib.Path]:
return {
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("#")
}
@@ -47,7 +79,7 @@ def main() -> int:
sources = source_paths()
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}
- baseline = baseline_paths()
+ baseline = baseline_paths(BASELINE_PATH)
available_baseline = {
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.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():
if path != CENTRAL_LAYOUT and LOCAL_PAGE_LAYOUT.search(text):
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:
absolute_path = REPOS_ROOT / path
@@ -77,9 +130,21 @@ def main() -> int:
if RAW_PAGE_FRAME in text:
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 " int:
print(
"Shared WebUI layout contract passed: "
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
diff --git a/tools/checks/shared-webui-layout-baseline.txt b/tools/checks/shared-webui-layout-baseline.txt
index cc218ac..5320950 100644
--- a/tools/checks/shared-webui-layout-baseline.txt
+++ b/tools/checks/shared-webui-layout-baseline.txt
@@ -1,22 +1,6 @@
# 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.
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-docs/webui/src/features/docs/DocsPage.tsx
govoplan-mail/webui/src/features/mail/MailBouncePage.tsx
diff --git a/tools/checks/shared-webui-workspace-baseline.txt b/tools/checks/shared-webui-workspace-baseline.txt
new file mode 100644
index 0000000..3760aba
--- /dev/null
+++ b/tools/checks/shared-webui-workspace-baseline.txt
@@ -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