chore(webui): enforce semantic interface patterns
This commit is contained in:
@@ -89,6 +89,17 @@ 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.
|
||||
|
||||
`PageActionBar` is the semantic action contract for headed pages;
|
||||
`WorkspaceActionBar` applies the identical ordering and lifecycle rules to a
|
||||
full canvas and its collection, detail, and editor panes. Reload is always the
|
||||
leading action on a refreshable projection. Help and ordinary task actions
|
||||
follow contextual controls; Create is the far-right collection action;
|
||||
destructive actions occupy a named separated group; an editor ends with
|
||||
Discard and Save, with Save at the far right. Editor state is explicit:
|
||||
`clean`, `dirty`, `invalid`, `saving`, `save-failed`, or `conflict`. Lower-level
|
||||
`ActionToolbar` remains appropriate for a section-local view switch or compact
|
||||
control group, but it must not recreate page or pane action placement.
|
||||
|
||||
Composite workspaces whose selected contribution supplies its own semantic
|
||||
heading may use `PageLayout` with its visible header delegated. This preserves
|
||||
the central inset, loading boundary, help identity, and content frame without
|
||||
@@ -120,6 +131,10 @@ The current structural vocabulary is:
|
||||
distribution, density, grouping, panel/section surfaces, accessible toolbar
|
||||
naming, help identity, and responsive wrapping. Modules may add
|
||||
domain-specific presentation; they do not recreate the flex/wrap skeleton.
|
||||
- `PageActionBar` and `WorkspaceActionBar` own semantic ordering, Reload,
|
||||
editor persistence state, destructive separation, and page/pane scope. A
|
||||
module supplies action behavior, authority, blocker reasons, and wording;
|
||||
it does not assemble another panel-header action convention.
|
||||
- `WorkspaceFrame` and `WorkspaceLayout` own application-viewport framing,
|
||||
surfaces, overflow, list/detail and navigation/content pane geometry,
|
||||
accessible region identity, and responsive pane behavior. Modules own only
|
||||
@@ -144,8 +159,13 @@ The current structural vocabulary is:
|
||||
validation, permissions, or domain wording.
|
||||
- `MetricGrid` owns the responsive grouping around `MetricCard`: fixed one-to-five
|
||||
columns or auto-fit, minimum card width, density, surrounding rhythm, and a
|
||||
named collapse point. Modules provide the metric, tone, and consequence;
|
||||
they do not recreate the group grid or reach across module CSS to size it.
|
||||
named collapse point. `MetricCard.drilldown` provides an explicit link or
|
||||
in-page action when an authorized underlying detail helps the user act; it
|
||||
names that destination and preserves the current scope and filters. The card
|
||||
itself is never the hidden click target. Derived, privacy-suppressed,
|
||||
non-enumerable, and purely informational aggregates remain inert. Modules
|
||||
provide the metric, tone, destination, and consequence; they do not recreate
|
||||
the group grid or reach across module CSS to size it.
|
||||
- `DescriptionList` and `DescriptionItem` own semantic property presentation.
|
||||
The stacked variant supports compact multi-column facts; the inline variant
|
||||
supports one-column term/value rows with a standard term width. Both own
|
||||
@@ -230,7 +250,8 @@ one.
|
||||
names the object. Do not duplicate that action in a permanently visible side
|
||||
panel. A side panel used as the creation surface appears for creation and is
|
||||
otherwise absent or returns to its documented non-creation purpose.
|
||||
- Put filters beside the list or pane they affect. Put bulk actions immediately
|
||||
- Put filters beside the list or pane they affect. Put collection, detail, and
|
||||
editor-pane actions in `WorkspaceActionBar` with the matching scope. Put bulk actions immediately
|
||||
above or beside the current selection. Put object actions with the object
|
||||
detail, not in the global title bar.
|
||||
- Full-page create and edit surfaces put their persistent action cluster in the
|
||||
|
||||
@@ -78,10 +78,11 @@ matters.
|
||||
|
||||
A Quick Access contribution declares:
|
||||
|
||||
- a stable id, category and human label;
|
||||
- contract version 1, a stable id, category and human label;
|
||||
- icon, order and optional badge/summary provider;
|
||||
- required permissions and optional dependencies;
|
||||
- accepted context references and produced return references;
|
||||
- global or active-object availability, accepted context-reference kinds and
|
||||
produced result-reference kinds;
|
||||
- an owner-rendered bounded WebUI surface and full-page fallback route;
|
||||
- View surface, help context and availability explanation;
|
||||
- whether the contribution supports preview, create, select or resume.
|
||||
@@ -92,6 +93,22 @@ resources and a safe return location. The owner reauthorizes every read and
|
||||
effect. Credentials, protected content and permission decisions are never
|
||||
embedded in launch context.
|
||||
|
||||
Launch-context version 2 identifies reference contract version 1 and carries
|
||||
the exact resolved View revision plus optional recommended and focused tool
|
||||
ids. Recommendations affect order and emphasis only. Focus narrows the rail
|
||||
only when at least one focused contribution survives module enablement,
|
||||
configuration, context compatibility and authorization; otherwise the normal
|
||||
effective rail remains available. Workflow gets the same behavior by resolving
|
||||
the exact View revision instead of acquiring separate presentation authority.
|
||||
|
||||
An owner-rendered tool explicitly returns result contract version 1 as either
|
||||
`completed` with an action and typed owner reference, or `cancelled` with a
|
||||
reason. The shell correlates the result with the source and tool, rejects
|
||||
cross-tenant or undeclared reference kinds, and does not interpret closing the
|
||||
drawer as completion. Owner modules validate, persist, recover and audit their
|
||||
own effects. The overlay leaves the host route mounted, so unsaved host-page
|
||||
state is preserved; the full-page route remains the bounded-work fallback.
|
||||
|
||||
## Effective Configuration
|
||||
|
||||
The effective rail is resolved from:
|
||||
@@ -125,9 +142,9 @@ workspace layouts do not resize unexpectedly; a later explicit pinned mode may
|
||||
reserve layout width on sufficiently wide screens.
|
||||
|
||||
The drawer preserves host-page state, has a deterministic focus return, closes
|
||||
with Escape, supports keyboard traversal, and provides an explicit full-page
|
||||
open action. Mobile and narrow layouts use the same category/configuration
|
||||
semantics in a bottom sheet or compact menu.
|
||||
with Escape, supports keyboard traversal, and provides explicit completion,
|
||||
cancellation and full-page actions. Mobile and narrow layouts use the same
|
||||
category/configuration semantics in a bottom sheet or compact menu.
|
||||
|
||||
## Product Areas
|
||||
|
||||
|
||||
@@ -28,59 +28,22 @@ 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 = (
|
||||
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"),
|
||||
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"),
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/AdminPage.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
||||
pathlib.Path("govoplan-mail/webui/src/features/mail/MailBouncePage.tsx"),
|
||||
pathlib.Path("govoplan-payments/webui/src/features/payments/PaymentsPage.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"),
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/AdminPage.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
||||
pathlib.Path("govoplan-organizations/webui/src/features/organizations/OrganizationsPage.tsx"),
|
||||
)
|
||||
HEADERLESS_PAGE_LAYOUT_CONSUMERS = {
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/AdminPage.tsx"),
|
||||
CENTRAL_ACTION_BARS = {
|
||||
pathlib.Path("govoplan-core/webui/src/components/PageActionBar.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/components/WorkspaceActionBar.tsx"),
|
||||
}
|
||||
EDITOR_PAGE_CONSUMERS = (
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-dashboard/webui/src/features/dashboard/DashboardPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/AttachmentsDataPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignFieldsPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.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/TemplateDataPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx"),
|
||||
SEMANTIC_ACTION_USAGE = re.compile(r"<(?:Page|Workspace)ActionBar\b")
|
||||
EDITOR_ACTION_USAGE = re.compile(
|
||||
r"<(?:Page|Workspace)ActionBar\b[\s\S]{0,1200}?variant=\"editor\""
|
||||
)
|
||||
DESTRUCTIVE_PAGE_CONSUMERS = (
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx"),
|
||||
PANEL_HEADER_ACTION_TOOLBAR = re.compile(
|
||||
r"<ActionToolbar\b[^>]*\bsurface=\"panel-header\""
|
||||
)
|
||||
UNSAVED_GUARD_MARKERS = (
|
||||
"useUnsavedDraftGuard",
|
||||
"useCampaignDraftEditor",
|
||||
"useRegisterUnsavedChanges",
|
||||
"semantic-editor-guard:",
|
||||
)
|
||||
|
||||
|
||||
@@ -108,6 +71,22 @@ def relative(path: pathlib.Path) -> pathlib.Path:
|
||||
def main() -> int:
|
||||
sources = source_paths()
|
||||
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
|
||||
page_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path != CENTRAL_LAYOUT and PAGE_LAYOUT_USAGE.search(text)
|
||||
}
|
||||
workspace_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path != CENTRAL_WORKSPACE_LAYOUT and "<WorkspaceLayout" in text
|
||||
}
|
||||
action_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path not in CENTRAL_ACTION_BARS and SEMANTIC_ACTION_USAGE.search(text)
|
||||
}
|
||||
editor_consumers = {
|
||||
path for path, text in source_text.items()
|
||||
if path not in CENTRAL_ACTION_BARS and EDITOR_ACTION_USAGE.search(text)
|
||||
}
|
||||
raw_frames = {path for path, text in source_text.items() if RAW_PAGE_FRAME in text}
|
||||
baseline = baseline_paths(BASELINE_PATH)
|
||||
available_baseline = {
|
||||
@@ -160,9 +139,9 @@ def main() -> int:
|
||||
if (
|
||||
page_layout_count
|
||||
and "actions=" in text
|
||||
and path not in HEADERLESS_PAGE_LAYOUT_CONSUMERS
|
||||
and path != pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx")
|
||||
and "<PageActionBar" not in text
|
||||
and "semantic-page-actions: delegated" not in text
|
||||
):
|
||||
errors.append(
|
||||
f"Headed page actions must use the semantic PageActionBar: {path}"
|
||||
@@ -172,51 +151,31 @@ def main() -> int:
|
||||
f"Ambiguous consequential action slots are forbidden; use destructiveActions: {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}")
|
||||
if (
|
||||
path not in CENTRAL_ACTION_BARS
|
||||
and PANEL_HEADER_ACTION_TOOLBAR.search(text)
|
||||
):
|
||||
errors.append(
|
||||
"Panel-header actions must use WorkspaceActionBar so their ordering, "
|
||||
f"state, and destructive separation remain semantic: {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}")
|
||||
if "<WorkspaceFrame" in text and not SEMANTIC_ACTION_USAGE.search(text):
|
||||
errors.append(
|
||||
f"WorkspaceFrame routes must declare a semantic page or pane action bar: {path}"
|
||||
)
|
||||
|
||||
for path in EDITOR_PAGE_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
continue
|
||||
text = absolute_path.read_text(encoding="utf-8")
|
||||
for required in ('variant="editor"', "dirty=", "discardAction=", "saveAction="):
|
||||
for path in editor_consumers:
|
||||
text = source_text[path]
|
||||
for required in ('variant="editor"', "state=", "discardAction=", "saveAction="):
|
||||
if required not in text:
|
||||
errors.append(f"Editor page is missing {required}: {path}")
|
||||
if path.name != "CampaignDraftPageScaffold.tsx" and not any(
|
||||
guard in text
|
||||
for guard in (
|
||||
"useUnsavedDraftGuard",
|
||||
"useCampaignDraftEditor",
|
||||
"useRegisterUnsavedChanges",
|
||||
)
|
||||
):
|
||||
if not any(guard in text for guard in UNSAVED_GUARD_MARKERS):
|
||||
errors.append(f"Editor page is missing an unsaved-change guard: {path}")
|
||||
|
||||
for path in DESTRUCTIVE_PAGE_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
continue
|
||||
text = absolute_path.read_text(encoding="utf-8")
|
||||
if "destructiveActions=" not in text or 'variant="danger"' not in text:
|
||||
errors.append(f"Destructive page actions lost their separated slot: {path}")
|
||||
for path, text in source_text.items():
|
||||
if "destructiveActions=" in text and 'variant="danger"' not in text:
|
||||
errors.append(f"Destructive page actions must contain a danger action: {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"):
|
||||
@@ -225,6 +184,8 @@ def main() -> int:
|
||||
errors.append("Core must export WorkspaceLayout from @govoplan/core-webui.")
|
||||
if core_index.exists() and "PageActionBar" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export PageActionBar from @govoplan/core-webui.")
|
||||
if core_index.exists() and "WorkspaceActionBar" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export WorkspaceActionBar from @govoplan/core-webui.")
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
@@ -232,11 +193,12 @@ def main() -> int:
|
||||
|
||||
print(
|
||||
"Shared WebUI layout contract passed: "
|
||||
f"{len(REQUIRED_CONSUMERS)} central consumers, "
|
||||
f"{len(page_consumers)} discovered page consumers, "
|
||||
f"{sum(len(PAGE_LAYOUT_USAGE.findall(text)) for text in source_text.values())} semantic pages, "
|
||||
f"{len(EDITOR_PAGE_CONSUMERS)} guarded editor consumers, "
|
||||
f"{len(action_consumers)} semantic action consumers, "
|
||||
f"{len(editor_consumers)} guarded editor consumers, "
|
||||
f"{len(raw_frames)} registered legacy page-frame files; "
|
||||
f"{len(REQUIRED_WORKSPACE_CONSUMERS)} workspace consumers, "
|
||||
f"{len(workspace_consumers)} discovered workspace consumers, "
|
||||
f"{len(raw_workspaces)} registered legacy workspace files."
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -125,7 +125,6 @@ REQUIRED_CONSUMERS = {
|
||||
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": (
|
||||
|
||||
Reference in New Issue
Block a user