chore: enforce shared WebUI layouts
This commit is contained in:
@@ -51,6 +51,55 @@ rules:
|
|||||||
- Preserve a stable way back to the containing object and the broader system.
|
- Preserve a stable way back to the containing object and the broader system.
|
||||||
- Do not let navigation, selection, or a view switch imply consent.
|
- Do not let navigation, selection, or a view switch imply consent.
|
||||||
|
|
||||||
|
## Shared Component And Layout Architecture
|
||||||
|
|
||||||
|
Core owns the reusable WebUI vocabulary; modules own domain composition and
|
||||||
|
behavior. Centralization follows four layers:
|
||||||
|
|
||||||
|
| Layer | Owner | Examples | Rule |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Foundation | Core | theme tokens, spacing, typography, focus and responsive breakpoints | Modules consume the contract and do not redefine it. |
|
||||||
|
| Primitives | Core | buttons, fields, dialogs, alerts, cards, tables, loading, empty and blocked states | A matching primitive is reused rather than copied locally. |
|
||||||
|
| Structural layouts | Core | page frame and header, action region, workspace panes, toolbars, grids, form sections and dialog anatomy | Layout owns geometry, scroll, responsive collapse and accessibility, but no domain decisions. |
|
||||||
|
| Domain compositions | Owning module | a campaign review, mailbox, records explorer or operations dashboard | Modules select shared pieces, bind data and permissions, and retain domain wording and consequences. |
|
||||||
|
|
||||||
|
A component belongs in Core when it is used or expected in more than one
|
||||||
|
module and central ownership materially protects accessibility, responsive
|
||||||
|
behavior, localization, contextual help, theming, or interaction consistency.
|
||||||
|
A component stays module-owned when its API would otherwise encode a domain
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
state skeleton under a module-prefixed name. If an archetype cannot be
|
||||||
|
expressed by the central API, extend the central contract or record a bounded
|
||||||
|
exception before introducing local structure.
|
||||||
|
|
||||||
|
Migration is incremental and enforceable:
|
||||||
|
|
||||||
|
1. inventory copied structures and register existing debt;
|
||||||
|
2. introduce the smallest domain-neutral Core contract with accessibility,
|
||||||
|
help, localization, theme and narrow-layout tests;
|
||||||
|
3. migrate representative Core and optional-module consumers;
|
||||||
|
4. reject new copies while removing registered debt in bounded module batches;
|
||||||
|
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.
|
||||||
|
|
||||||
## Surface Archetypes
|
## Surface Archetypes
|
||||||
|
|
||||||
Choose an archetype from the task, then specialize it for the domain. A route
|
Choose an archetype from the task, then specialize it for the domain. A route
|
||||||
@@ -101,7 +150,8 @@ one.
|
|||||||
creation panel.
|
creation panel.
|
||||||
- Workflow, configuration, dashboard, and explanatory pages may use a heading.
|
- Workflow, configuration, dashboard, and explanatory pages may use a heading.
|
||||||
The heading names the task or scoped object and contains only route-level
|
The heading names the task or scoped object and contains only route-level
|
||||||
actions.
|
actions. Use the Core `PageLayout` contract for the frame and `PageHeader`
|
||||||
|
only when a full-canvas archetype owns its own scrolling.
|
||||||
- Put a collection-wide create action in the heading of the collection it
|
- Put a collection-wide create action in the heading of the collection it
|
||||||
affects. Use a short, specific label such as `Add` when the heading already
|
affects. Use a short, specific label such as `Add` when the heading already
|
||||||
names the object. Do not duplicate that action in a permanently visible side
|
names the object. Do not duplicate that action in a permanently visible side
|
||||||
|
|||||||
@@ -89,6 +89,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")'
|
"$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"
|
"$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
||||||
|
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-layouts.py"
|
||||||
"$PYTHON" -m unittest tests.test_module_system
|
"$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-connectors/tests
|
||||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Keep raw module page frames from growing while shared layouts are adopted."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
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")
|
||||||
|
RAW_PAGE_FRAME = 'className="content-pad workspace-data-page'
|
||||||
|
LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b")
|
||||||
|
CENTRAL_LAYOUT = pathlib.Path("govoplan-core/webui/src/components/PageLayout.tsx")
|
||||||
|
REQUIRED_CONSUMERS = (
|
||||||
|
pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx"),
|
||||||
|
pathlib.Path("govoplan-core/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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def baseline_paths() -> set[pathlib.Path]:
|
||||||
|
return {
|
||||||
|
pathlib.Path(line.strip())
|
||||||
|
for line in BASELINE_PATH.read_text(encoding="utf-8").splitlines()
|
||||||
|
if line.strip() and not line.lstrip().startswith("#")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 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()
|
||||||
|
available_baseline = {
|
||||||
|
path for path in baseline if (REPOS_ROOT / path.parts[0]).is_dir()
|
||||||
|
}
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
unexpected = sorted(raw_frames - available_baseline)
|
||||||
|
if unexpected:
|
||||||
|
errors.append("New raw page frames must use @govoplan/core-webui PageLayout:")
|
||||||
|
errors.extend(f"- {path}" for path in unexpected)
|
||||||
|
|
||||||
|
resolved = sorted(available_baseline - raw_frames)
|
||||||
|
if resolved:
|
||||||
|
errors.append("Remove migrated page frames from the shared-layout baseline:")
|
||||||
|
errors.extend(f"- {path}" for path in resolved)
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
for path in REQUIRED_CONSUMERS:
|
||||||
|
absolute_path = REPOS_ROOT / path
|
||||||
|
if not absolute_path.exists():
|
||||||
|
continue
|
||||||
|
text = absolute_path.read_text(encoding="utf-8")
|
||||||
|
if "<PageLayout" not in text:
|
||||||
|
errors.append(f"Required shared-layout consumer no longer uses PageLayout: {path}")
|
||||||
|
if RAW_PAGE_FRAME in text:
|
||||||
|
errors.append(f"Required shared-layout consumer restored the raw page frame: {path}")
|
||||||
|
|
||||||
|
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"):
|
||||||
|
errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n".join(errors), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Shared WebUI layout contract passed: "
|
||||||
|
f"{len(REQUIRED_CONSUMERS)} central consumers, "
|
||||||
|
f"{len(raw_frames)} registered legacy page-frame files."
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 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
|
||||||
Reference in New Issue
Block a user