diff --git a/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md b/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md index 292e16c..78fb4b2 100644 --- a/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md +++ b/docs/architecture/INTERFACE_PATTERN_LANGUAGE.md @@ -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 diff --git a/docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md b/docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md index e481161..98b6807 100644 --- a/docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md +++ b/docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md @@ -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 diff --git a/tools/checks/check-shared-webui-layouts.py b/tools/checks/check-shared-webui-layouts.py index 459a4bb..efae144 100644 --- a/tools/checks/check-shared-webui-layouts.py +++ b/tools/checks/check-shared-webui-layouts.py @@ -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"]*\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 " 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 " 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 " 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 diff --git a/tools/checks/check-shared-webui-primitives.py b/tools/checks/check-shared-webui-primitives.py index b77ca38..cad2dc2 100644 --- a/tools/checks/check-shared-webui-primitives.py +++ b/tools/checks/check-shared-webui-primitives.py @@ -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": (