diff --git a/docs/ACCESSIBILITY_REVIEW.md b/docs/ACCESSIBILITY_REVIEW.md
index 5047425..82e9aa0 100644
--- a/docs/ACCESSIBILITY_REVIEW.md
+++ b/docs/ACCESSIBILITY_REVIEW.md
@@ -1,8 +1,11 @@
# Campaign Accessibility Review
The Campaign WebUI uses Core's semantic `Button`, `Dialog`, `DataGrid`, form,
-alert, and segmented-control components. Core owns focus trapping, focus return,
-Escape handling, labels, disabled state, and keyboard behavior for those shared
+alert, and segmented-control components. Its page frames use Core `PageLayout`
+and its full-canvas navigation/content shells use `WorkspaceLayout`, so pane
+scrolling, sticky headings, action collapse, narrow-layout behavior, and help
+scope remain platform-owned. Core also owns focus trapping, focus return,
+Escape handling, labels, disabled state, and keyboard behavior for the shared
primitives.
## Repeatable review matrix
diff --git a/webui/src/features/campaigns/AttachmentsDataPage.tsx b/webui/src/features/campaigns/AttachmentsDataPage.tsx
index e905f1a..d4aac9c 100644
--- a/webui/src/features/campaigns/AttachmentsDataPage.tsx
+++ b/webui/src/features/campaigns/AttachmentsDataPage.tsx
@@ -4,7 +4,7 @@ import { useGuardedNavigate, usePlatformModuleInstalled, usePlatformUiCapability
import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
@@ -247,22 +247,24 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
return (
-
-
-
-
i18n:govoplan-campaign.attachments.6771ade6
-
-
-
+
}
+ headerLoading={loading}
+ error={error}
+ actions={
+ <>
{filesModuleInstalled &&
navigate("/files")}>i18n:govoplan-campaign.manage_files.90a419f7 }
void discardDraft()} disabled={loading}>Discard
saveDraft("manual")} disabled={!canSave}>i18n:govoplan-campaign.save.efc007a3
-
-
-
- {error &&
{error} }
- {localError &&
{localError} }
- {locked &&
}
+ >
+ }
+ notices={(localError || locked) ? <>
+ {localError &&
{localError} }
+ {locked &&
}
+ > : undefined}
+ >
<>
@@ -452,7 +454,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
onConfirm={confirmIndividualDisable}
onCancel={() => setIndividualDisable(null)} />
- );
+ );
}
diff --git a/webui/src/features/campaigns/CampaignAuditPage.tsx b/webui/src/features/campaigns/CampaignAuditPage.tsx
index f1987c0..755bdc0 100644
--- a/webui/src/features/campaigns/CampaignAuditPage.tsx
+++ b/webui/src/features/campaigns/CampaignAuditPage.tsx
@@ -1,11 +1,8 @@
import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
-import { DismissibleAlert } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
-import { LoadingFrame } from "@govoplan/core-webui";
-import { ActionBlockerHint, DocumentationHelpLink } from "@govoplan/core-webui";
+import { ActionBlockerHint, DocumentationHelpLink, PageLayout } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
export default function CampaignAuditPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
@@ -13,20 +10,17 @@ export default function CampaignAuditPage({ settings, campaignId }: {settings: A
const version = data.currentVersion;
return (
-
-
-
-
i18n:govoplan-campaign.audit_log.3cfc5f1c
-
-
-
+
}
+ loading={loading}
+ loadingLabel="i18n:govoplan-campaign.loading_audit_data.af52b968"
+ error={error}
+ actions={
void reload({ force: true })} disabled={loading}>Discard
-
-
-
- {error &&
{error} }
-
-
+ }
+ >
-
-
);
+ );
}
diff --git a/webui/src/features/campaigns/CampaignFieldsPage.tsx b/webui/src/features/campaigns/CampaignFieldsPage.tsx
index 21759c4..f4cf926 100644
--- a/webui/src/features/campaigns/CampaignFieldsPage.tsx
+++ b/webui/src/features/campaigns/CampaignFieldsPage.tsx
@@ -1,7 +1,7 @@
import { useMemo, useRef } from "react";
import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import LockedVersionNotice from "./components/LockedVersionNotice";
@@ -150,22 +150,24 @@ export default function CampaignFieldsPage({ settings, campaignId }: {settings:
return (
-
-
-
-
i18n:govoplan-campaign.fields.e8b68527
-
-
-
+
}
+ headerLoading={loading}
+ error={error}
+ actions={
+ <>
void discardDraft()} disabled={loading}>Discard
i18n:govoplan-campaign.save.efc007a3
-
-
-
- {error &&
{error} }
- {localError &&
{localError} }
- {fieldNameWarning &&
{fieldNameWarning} }
- {locked &&
}
+ >
+ }
+ notices={(localError || fieldNameWarning || locked) ? <>
+ {localError &&
{localError} }
+ {fieldNameWarning &&
{fieldNameWarning} }
+ {locked &&
}
+ > : undefined}
+ >
<>
@@ -184,7 +186,7 @@ export default function CampaignFieldsPage({ settings, campaignId }: {settings:
>
-
);
+ );
}
diff --git a/webui/src/features/campaigns/CampaignJsonView.tsx b/webui/src/features/campaigns/CampaignJsonView.tsx
index 3f12dd0..2f047a6 100644
--- a/webui/src/features/campaigns/CampaignJsonView.tsx
+++ b/webui/src/features/campaigns/CampaignJsonView.tsx
@@ -1,10 +1,8 @@
import type { ApiSettings } from "../../types";
import { Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
-import { DismissibleAlert } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { DismissibleAlert, PageLayout } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
-import { LoadingFrame } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { asRecord, getCampaignJson } from "./utils/campaignView";
import { downloadJson, safeFileStem } from "./utils/draftEditor";
@@ -17,28 +15,30 @@ export default function CampaignJsonView({ settings, campaignId }: {settings: Ap
const filename = `${safeFileStem(String(campaign.id || data.campaign?.external_id || campaignId))}.json`;
return (
-
-
-
-
i18n:govoplan-campaign.json.031a4e76
-
-
-
+
}
+ loading={loading}
+ loadingLabel="i18n:govoplan-campaign.loading_json.812c7a50"
+ error={error}
+ actions={
+ <>
void reload({ force: true })} disabled={loading}>Discard
downloadJson(filename, campaignJson)} disabled={!version}>i18n:govoplan-campaign.download_json.d296a30a
-
-
- {error &&
{error} }
-
- This expert view contains the complete authorized campaign configuration,
- including recipient and message fields that may contain personal data.
- Download and share it only for an authorized purpose.
-
-
+ >
+ }
+ notices={
+
+ This expert view contains the complete authorized campaign configuration,
+ including recipient and message fields that may contain personal data.
+ Download and share it only for an authorized purpose.
+
+ }
+ >
{!loading || version ? {JSON.stringify(campaignJson, null, 2)} : {"{}"} }
-
-
);
+ );
}
diff --git a/webui/src/features/campaigns/CampaignListPage.tsx b/webui/src/features/campaigns/CampaignListPage.tsx
index 2d85b2a..8931400 100644
--- a/webui/src/features/campaigns/CampaignListPage.tsx
+++ b/webui/src/features/campaigns/CampaignListPage.tsx
@@ -6,10 +6,8 @@ import type { ApiSettings } from "../../types";
import { Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
-import { PageScrollViewport } from "@govoplan/core-webui";
-import { DismissibleAlert, TableActionGroup, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui";
+import { PageLayout, TableActionGroup, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui";
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
import { createNewCampaign, listCampaignsDelta, type CampaignDeltaResponse } from "../../api/campaigns";
import type { CampaignListItem } from "../../types";
@@ -144,22 +142,22 @@ export default function CampaignListPage({ settings }: {settings: ApiSettings;})
return (
-
-
- {error &&
{error} }
-
-
-
-
i18n:govoplan-campaign.all_campaigns.2bd1ee3a
-
{lastLoadedAt ? i18nMessage("i18n:govoplan-campaign.last_loaded_value.35ef046a", { value0: lastLoadedAt }) : "i18n:govoplan-campaign.not_loaded_yet.9968c191"}
-
-
+
{lastLoadedAt ? i18nMessage("i18n:govoplan-campaign.last_loaded_value.35ef046a", { value0: lastLoadedAt }) : "i18n:govoplan-campaign.not_loaded_yet.9968c191"}}
+ headerLoading={loading}
+ error={error}
+ actions={
+ <>
void load(null)} disabled={loading}>Discard
{creating ? "i18n:govoplan-campaign.creating.94d7d8ee" : "i18n:govoplan-campaign.new_campaign.aaf9a8a4"}
-
-
+ >
+ }
+ >
@@ -186,8 +184,7 @@ export default function CampaignListPage({ settings }: {settings: ApiSettings;})
}
-
- );
+ );
}
diff --git a/webui/src/features/campaigns/CampaignModulePage.tsx b/webui/src/features/campaigns/CampaignModulePage.tsx
index 46f0f62..b981707 100644
--- a/webui/src/features/campaigns/CampaignModulePage.tsx
+++ b/webui/src/features/campaigns/CampaignModulePage.tsx
@@ -1,6 +1,7 @@
import { lazy } from "react";
import {
ModuleSubnav,
+ WorkspaceLayout,
hasAnyScope,
useGuardedNavigate,
type ApiSettings,
@@ -64,15 +65,19 @@ export default function CampaignModulePage({
}
return (
-
-
-
+ }
+ primaryLabel="Campaign module sections"
+ contentLabel="Campaign module content"
+ interfaceId="campaign.module-workspace"
+ helpModuleId="campaign"
+ >
{active === "queue"
?
: active === "reports"
?
: }
-
-
+
);
}
diff --git a/webui/src/features/campaigns/CampaignOverviewPage.tsx b/webui/src/features/campaigns/CampaignOverviewPage.tsx
index 7ebba58..83ae27d 100644
--- a/webui/src/features/campaigns/CampaignOverviewPage.tsx
+++ b/webui/src/features/campaigns/CampaignOverviewPage.tsx
@@ -9,7 +9,7 @@ import { Dialog } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui";
import { DismissibleAlert, TableActionGroup, hasScope, i18nMessage, useGuardedNavigate, useUnsavedDraftGuard } from "@govoplan/core-webui";
@@ -332,13 +332,15 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
}
return (
-
-
-
-
{campaign?.name || "i18n:govoplan-campaign.overview.0efc2e6b"}
-
i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113
-
-
+
i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113}
+ headerLoading={loading}
+ error={error}
+ success={message}
+ actions={
+ <>
{canCopy && data.currentVersion && void prepareLifecycleAction("copy_campaign", data.currentVersion ?? undefined)}
disabled={loading || savingIdentity || lockBusy || lifecycleBusy || identityDirty}
@@ -371,11 +373,9 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
}
void discardOverview()} disabled={loading || savingIdentity || lockBusy}>Discard
void saveIdentity()} disabled={!campaign || !identityDirty || savingIdentity}>{savingIdentity ? "i18n:govoplan-campaign.saving.56a2285c" : "i18n:govoplan-campaign.save.efc007a3"}
-
-
-
- {error &&
{error} }
- {message &&
{message} }
+ >
+ }
+ >
@@ -618,7 +618,7 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
onCancel={() => setPendingLockAction(null)}
onConfirm={() => void applyLockAction()} />
-
);
+ );
}
diff --git a/webui/src/features/campaigns/CampaignReportPage.tsx b/webui/src/features/campaigns/CampaignReportPage.tsx
index 12d0d3c..343cc91 100644
--- a/webui/src/features/campaigns/CampaignReportPage.tsx
+++ b/webui/src/features/campaigns/CampaignReportPage.tsx
@@ -21,7 +21,7 @@ import { DataGrid, type DataGridColumn, type DataGridListOption, type DataGridQu
import { Dialog } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
import { LoadingFrame, TableActionGroup, ToggleSwitch, i18nMessage } from "@govoplan/core-webui";
@@ -445,20 +445,21 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
[busyAction, retryFailedSynchronously]);
return (
-
-
-
-
i18n:govoplan-campaign.report.ee45c303
-
-
-
+
}
+ headerLoading={loading}
+ error={error || actionError}
+ success={actionMessage}
+ actions={
+ <>
void exportCsv()} disabled={busyAction === "csv"}>i18n:govoplan-campaign.download_csv.eaa216ad
setEmailOpen(true)}>i18n:govoplan-campaign.email_report.ee3e7091
void reloadAll()} disabled={loading || jobsLoading}>Discard
-
-
- {(error || actionError) &&
{error || actionError} }
- {actionMessage &&
{actionMessage} }
+ >
+ }
+ >
@@ -647,7 +648,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
onConfirm={() => void reconcileOutcome()}
onCancel={() => setReconcile(null)} />
-
);
+ );
}
diff --git a/webui/src/features/campaigns/CampaignWorkspace.tsx b/webui/src/features/campaigns/CampaignWorkspace.tsx
index f28530c..5c1d45d 100644
--- a/webui/src/features/campaigns/CampaignWorkspace.tsx
+++ b/webui/src/features/campaigns/CampaignWorkspace.tsx
@@ -2,6 +2,7 @@ import { lazy, useEffect, useState } from "react";
import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from "react-router";
import {
ConcurrencyConflictProvider,
+ WorkspaceLayout,
useGuardedNavigate
} from "@govoplan/core-webui";
import type { ApiSettings, AuthInfo, CampaignWorkspaceSection } from "../../types";
@@ -85,9 +86,13 @@ function CampaignWorkspaceInner({ settings, auth }: { settings: ApiSettings; aut
}
return (
-
-
-
+ }
+ primaryLabel="Campaign sections"
+ contentLabel="Campaign workspace"
+ interfaceId="campaign.workspace"
+ helpModuleId="campaign"
+ >
} />
} />
@@ -121,8 +126,7 @@ function CampaignWorkspaceInner({ settings, auth }: { settings: ApiSettings; aut
} />
} />
-
-
+
);
}
diff --git a/webui/src/features/campaigns/GlobalSettingsPage.tsx b/webui/src/features/campaigns/GlobalSettingsPage.tsx
index e9eeef4..c4c6120 100644
--- a/webui/src/features/campaigns/GlobalSettingsPage.tsx
+++ b/webui/src/features/campaigns/GlobalSettingsPage.tsx
@@ -10,7 +10,7 @@ import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { PolicyRow } from "@govoplan/core-webui";
import { PolicyTable } from "@govoplan/core-webui";
@@ -178,21 +178,23 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
}
return (
-
-
-
-
+
}
+ headerLoading={loading}
+ error={error}
+ actions={
+ <>
void discardDraft()} disabled={loading}>Discard
saveDraft("manual")} disabled={!dirty || locked || !draft}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}
-
-
-
- {error &&
{error} }
- {localError &&
{localError} }
- {locked &&
}
+ >
+ }
+ notices={(localError || locked) ? <>
+ {localError &&
{localError} }
+ {locked &&
}
+ > : undefined}
+ >
{isPolicyView ?
@@ -500,7 +502,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
onClose={() => setPostboxTargetsOpen(false)}
/>
}
- );
+ );
}
diff --git a/webui/src/features/campaigns/MailSettingsPage.tsx b/webui/src/features/campaigns/MailSettingsPage.tsx
index d8f1fb3..bcf2016 100644
--- a/webui/src/features/campaigns/MailSettingsPage.tsx
+++ b/webui/src/features/campaigns/MailSettingsPage.tsx
@@ -8,7 +8,7 @@ import {
LoadingFrame,
MailServerFolderLookupResultView,
MetricCard,
- PageTitle,
+ PageLayout,
PasswordField,
ToggleSwitch,
usePlatformModuleInstalled,
@@ -326,21 +326,23 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
}
return (
-
-
-
-
{isPolicyView ? "i18n:govoplan-campaign.mail_policy.3eb5d32a" : "i18n:govoplan-campaign.mail_settings.19e07f55"}
-
-
-
+
}
+ headerLoading={loading}
+ error={error}
+ actions={
+ <>
{!isPolicyView &&
void saveDraft("manual")} disabled={!canSave}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"} }
void discardDraft()} disabled={loading}>i18n:govoplan-campaign.discard.36fff63c
-
-
-
- {error &&
{error} }
- {localError &&
{localError} }
- {locked &&
}
+ >
+ }
+ notices={(localError || locked) ? <>
+ {localError &&
{localError} }
+ {locked &&
}
+ > : undefined}
+ >
<>
@@ -501,7 +503,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
}
>
-
+
);
}
diff --git a/webui/src/features/campaigns/ReviewSendPage.tsx b/webui/src/features/campaigns/ReviewSendPage.tsx
index ed6f373..8077162 100644
--- a/webui/src/features/campaigns/ReviewSendPage.tsx
+++ b/webui/src/features/campaigns/ReviewSendPage.tsx
@@ -42,7 +42,7 @@ import { DataGrid, type DataGridQueryState } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui";
import { i18nMessage } from "@govoplan/core-webui";
@@ -1364,34 +1364,33 @@ export default function ReviewSendPage({
reviewedExplicitCount === explicitReviewCount;
return (
-
-
-
-
i18n:govoplan-campaign.review_send.1627617d
-
-
-
+
}
+ headerLoading={loading || Boolean(busy)}
+ error={error}
+ actions={
+ <>
void refreshQueueStatus(false)} disabled={!version || queueStatusLoading || Boolean(busy)}>
{queueStatusLoading ? "i18n:govoplan-campaign.refreshing_status.d8965739" : "i18n:govoplan-campaign.refresh_status.ade15a52"}
void reload({ force: true })} disabled={loading || Boolean(busy)}>Discard
-
-
-
- {error &&
{error} }
- {message &&
{message} }
-
-
- {version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion) &&
-
-
+ >
}
+ notices={(message || (version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion))) ? <>
+ {message &&
{message} }
+ {version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion) &&
+
}
+ > : undefined}
+ >
@@ -2113,6 +2112,6 @@ export default function ReviewSendPage({
onClose={() => setSelectedMockMessage(null)} />
}
- );
+ );
}
diff --git a/webui/src/features/campaigns/TemplateDataPage.tsx b/webui/src/features/campaigns/TemplateDataPage.tsx
index 429fd0f..1250763 100644
--- a/webui/src/features/campaigns/TemplateDataPage.tsx
+++ b/webui/src/features/campaigns/TemplateDataPage.tsx
@@ -16,7 +16,7 @@ import { Card } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { DismissibleAlert, SegmentedControl, ToggleSwitch, i18nMessage } from "@govoplan/core-webui";
import { WysiwygEditor, type WysiwygEditorHandle } from "@govoplan/core-webui/wysiwyg";
@@ -411,23 +411,25 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
return (
-
-
-
-
i18n:govoplan-campaign.template.3ec1ae06
-
-
-
+
}
+ headerLoading={loading}
+ error={error}
+ success={contentLibraryNotice}
+ actions={
+ <>
window.location.assign("/templates")}>i18n:govoplan-campaign.manage_templates.23688071
void discardDraft()} disabled={loading}>Discard
saveDraft("manual")} disabled={!dirty || locked || !draft}>i18n:govoplan-campaign.save.efc007a3
-
-
-
- {error &&
{error} }
- {localError &&
{localError} }
- {contentLibraryNotice &&
{contentLibraryNotice} }
- {locked &&
}
+ >
+ }
+ notices={(localError || locked) ? <>
+ {localError &&
{localError} }
+ {locked &&
}
+ > : undefined}
+ >
<>
@@ -783,7 +785,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
localFields={localFieldOptions}
globalFields={globalFieldOptions} />
- );
+ );
}
diff --git a/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx b/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
index 51519bd..84008d7 100644
--- a/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
+++ b/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
-import { Button, DismissibleAlert, LoadingFrame, PageTitle } from "@govoplan/core-webui";
+import { Button, DismissibleAlert, PageLayout } from "@govoplan/core-webui";
import type { ApiSettings } from "../../../types";
import type { CampaignVersionDetail, CampaignVersionListItem } from "../../../api/campaigns";
import LockedVersionNotice from "./LockedVersionNotice";
@@ -43,25 +43,26 @@ export default function CampaignDraftPageScaffold({
children
}: CampaignDraftPageScaffoldProps) {
return (
-
-
-
-
+
}
+ loading={loading || !draft}
+ loadingLabel="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50"
+ error={error || ""}
+ actions={
+ <>
Discard
i18n:govoplan-campaign.save.efc007a3
-
-
-
- {error &&
{error} }
- {localError &&
{localError} }
- {locked &&
}
-
-
- <>{children}>
-
-
);
+ >
+ }
+ notices={(localError || locked) ?
+ <>
+ {localError && {localError} }
+ {locked && }
+ > : undefined}
+ >
+ {children}
+ );
}
diff --git a/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx b/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
index bafd500..12bf390 100644
--- a/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
+++ b/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
@@ -1,6 +1,6 @@
import { Link } from "react-router";
import {
- PageTitle,
+ PageLayout,
WizardDirectory,
usePlatformUiCapabilities,
wizardEntriesForContext,
@@ -38,18 +38,20 @@ export default function WizardDirectoryPage({
});
return (
-
-
-
i18n:govoplan-campaign.guided_workflows
+
i18n:govoplan-campaign.back_to_overview.ec986cba
-
+ }
+ >
-
+
);
}
diff --git a/webui/src/features/operator/OperatorQueuePage.tsx b/webui/src/features/operator/OperatorQueuePage.tsx
index edf2a99..179466c 100644
--- a/webui/src/features/operator/OperatorQueuePage.tsx
+++ b/webui/src/features/operator/OperatorQueuePage.tsx
@@ -25,11 +25,9 @@ import {
type DataGridListOption,
type DataGridQueryState
} from "@govoplan/core-webui";
-import { DismissibleAlert } from "@govoplan/core-webui";
+import { DismissibleAlert, PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
-import { PageScrollViewport } from "@govoplan/core-webui";
import {
StatusBadge,
TableActionGroup,
@@ -757,22 +755,22 @@ export default function OperatorQueuePage({ settings, auth }: {settings: ApiSett
], [navigate, permissions.canOpenReport, selectedRow, selectedVersionId]);
return (
-
-
-
-
-
i18n:govoplan-campaign.operator_queue.72492fb5
+
i18n:govoplan-campaign.queue_state_retry_candidates_and_reconciliation_.1b592bbe
i18n:govoplan-campaign.queue_refreshes_automatically_while_delivery_is_active.df8dc205
-
-
+ >
+ }
+ error={error}
+ success={message}
+ actions={
void refreshAll()} disabled={loading || jobsLoading}>i18n:govoplan-campaign.refresh.56e3badc
-
-
-
- {error &&
{error} }
- {jobsError &&
{jobsError} }
- {message &&
{message} }
+ }
+ notices={jobsError ?
{jobsError} : undefined}
+ >
i18n:govoplan-campaign.queue_pressure.28eee15e
@@ -847,8 +845,7 @@ export default function OperatorQueuePage({ settings, auth }: {settings: ApiSett
if (cancelTarget) void runAction(cancelTarget, "cancel");
}}
onCancel={() => setCancelTarget(null)} />
-
- );
+ );
}
diff --git a/webui/src/features/reports/AggregateReportsPage.tsx b/webui/src/features/reports/AggregateReportsPage.tsx
index 3418807..b9c9be2 100644
--- a/webui/src/features/reports/AggregateReportsPage.tsx
+++ b/webui/src/features/reports/AggregateReportsPage.tsx
@@ -5,11 +5,9 @@ import {
Button,
Card,
DataGrid,
- DismissibleAlert,
LoadingFrame,
MetricCard,
- PageScrollViewport,
- PageTitle,
+ PageLayout,
StatusBadge,
TableActionGroup,
formatDateTime,
@@ -167,24 +165,21 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
const outcomes = report?.outcomes;
return (
-
-
-
-
-
i18n:govoplan-campaign.reports.88bc3fe3
-
i18n:govoplan-campaign.privacy_protected_campaign_outcomes_without_reci.5b8a100d
-
-
+
void Promise.all([loadCampaigns(), selectedFromUrl ? loadReport(selectedFromUrl) : Promise.resolve()])}
disabled={listLoading || reportLoading}
>
i18n:govoplan-campaign.refresh.56e3badc
-
-
-
- {error &&
{error} }
+ }
+ >
@@ -252,8 +247,7 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
>
)}
-
-
+
);
}
diff --git a/webui/src/features/templates/TemplatesPage.tsx b/webui/src/features/templates/TemplatesPage.tsx
index d60e4a2..d58d561 100644
--- a/webui/src/features/templates/TemplatesPage.tsx
+++ b/webui/src/features/templates/TemplatesPage.tsx
@@ -2,8 +2,7 @@ import { useMemo, useState } from "react";
import { ExternalLink } from "lucide-react";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
-import { PageScrollViewport } from "@govoplan/core-webui";
-import { PageTitle } from "@govoplan/core-webui";
+import { PageLayout, WorkspaceLayout } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui";
import { StatusBadge, TableActionGroup, i18nMessage } from "@govoplan/core-webui";
import { usePlatformLanguage } from "@govoplan/core-webui";
@@ -137,20 +136,25 @@ export default function TemplatesPage() {
if (selectedTemplate) {
return (
-
-
setSelectedId(null))} onSelect={setActive} />
-
-
-
-
-
{selectedTemplate.name}
-
{selectedTemplate.description}
-
-
+
setSelectedId(null))} onSelect={setActive} />}
+ primaryLabel="Template sections"
+ contentLabel="Template workspace"
+ interfaceId="campaign.template-workspace"
+ helpModuleId="campaign"
+ >
+
i18n:govoplan-campaign.duplicate.972d5737
i18n:govoplan-campaign.use_in_campaign.779163bc
-
-
+ >
+ }
+ >
{active === "overview" &&
}
{active === "content" &&
}
@@ -158,25 +162,23 @@ export default function TemplatesPage() {
{active === "preview" &&
}
{active === "usage" &&
}
{active === "versions" &&
}
-
-
- );
+
+ );
}
return (
-
-
-
-
-
i18n:govoplan-campaign.templates.f25b700e
-
i18n:govoplan-campaign.reusable_message_templates_open_a_template_to_ed.792e2afb
-
-
+
i18n:govoplan-campaign.import.d6fbc9d2
i18n:govoplan-campaign.export.f3e4fadb
-
-
+ >
+ }
+ >
-
- );
+ );
}
diff --git a/webui/tests/accessibility-contract.test.mjs b/webui/tests/accessibility-contract.test.mjs
index d2dff49..e687b36 100644
--- a/webui/tests/accessibility-contract.test.mjs
+++ b/webui/tests/accessibility-contract.test.mjs
@@ -17,6 +17,26 @@ const sources = sourceFiles(sourceRoot).map((file) => ({
value: fs.readFileSync(file, "utf8"),
}));
+for (const { file, value } of sources) {
+ assert.ok(
+ !value.includes('className="content-pad workspace-data-page'),
+ `${path.relative(sourceRoot, file)} must use Core PageLayout instead of a raw page frame`,
+ );
+ assert.ok(
+ !/
[...value.matchAll(/<(div|span|li|tr)\b[^>]*\bonClick\s*=/g)].map(
(match) => `${path.relative(sourceRoot, file)}:${match.index}`,