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