@actuate-media/cms-admin 0.73.0 → 0.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +3 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/LoginRoot.d.ts.map +1 -1
- package/dist/LoginRoot.js +2 -1
- package/dist/LoginRoot.js.map +1 -1
- package/dist/__tests__/components/theme-provider-accent.test.d.ts +2 -0
- package/dist/__tests__/components/theme-provider-accent.test.d.ts.map +1 -0
- package/dist/__tests__/components/theme-provider-accent.test.js +64 -0
- package/dist/__tests__/components/theme-provider-accent.test.js.map +1 -0
- package/dist/__tests__/views/portal-container.test.js +13 -0
- package/dist/__tests__/views/portal-container.test.js.map +1 -1
- package/dist/__tests__/views/post-fields-modal.render.test.js +3 -6
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.js +6 -0
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.js +15 -4
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/CoauthorResultPopover.js +1 -1
- package/dist/components/EditorSeoAside.d.ts +23 -0
- package/dist/components/EditorSeoAside.d.ts.map +1 -0
- package/dist/components/EditorSeoAside.js +21 -0
- package/dist/components/EditorSeoAside.js.map +1 -0
- package/dist/components/MediaPickerModal.js +1 -1
- package/dist/components/SEOConfigPanel.js +1 -1
- package/dist/components/SchedulePublishDialog.js +1 -1
- package/dist/components/SharePreviewLinkDialog.js +1 -1
- package/dist/components/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider.js +2 -1
- package/dist/components/ThemeProvider.js.map +1 -1
- package/dist/components/VersionHistory.js +1 -1
- package/dist/components/seo/EditorSeoScoreBadge.d.ts +7 -0
- package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -0
- package/dist/components/seo/EditorSeoScoreBadge.js +9 -0
- package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -0
- package/dist/components/seo/SeoAccordion.d.ts +16 -0
- package/dist/components/seo/SeoAccordion.d.ts.map +1 -0
- package/dist/components/seo/SeoAccordion.js +21 -0
- package/dist/components/seo/SeoAccordion.js.map +1 -0
- package/dist/components/seo/SeoAnalysisList.d.ts +7 -0
- package/dist/components/seo/SeoAnalysisList.d.ts.map +1 -0
- package/dist/components/seo/SeoAnalysisList.js +32 -0
- package/dist/components/seo/SeoAnalysisList.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +15 -4
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +155 -35
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/seo/SeoInsightsMetrics.d.ts +6 -0
- package/dist/components/seo/SeoInsightsMetrics.d.ts.map +1 -0
- package/dist/components/seo/SeoInsightsMetrics.js +16 -0
- package/dist/components/seo/SeoInsightsMetrics.js.map +1 -0
- package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +13 -3
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- package/dist/components/seo/SeoLinkingSection.d.ts +9 -0
- package/dist/components/seo/SeoLinkingSection.d.ts.map +1 -0
- package/dist/components/seo/SeoLinkingSection.js +44 -0
- package/dist/components/seo/SeoLinkingSection.js.map +1 -0
- package/dist/components/seo/primitives.d.ts.map +1 -1
- package/dist/components/seo/primitives.js +3 -3
- package/dist/components/seo/primitives.js.map +1 -1
- package/dist/components/ui/Card.js +5 -5
- package/dist/components/ui/Card.js.map +1 -1
- package/dist/components/ui/InspectorPane.js +3 -3
- package/dist/components/ui/InspectorPane.js.map +1 -1
- package/dist/components/ui/PanelSection.d.ts +16 -0
- package/dist/components/ui/PanelSection.d.ts.map +1 -0
- package/dist/components/ui/PanelSection.js +15 -0
- package/dist/components/ui/PanelSection.js.map +1 -0
- package/dist/lib/admin-root-element.d.ts +9 -0
- package/dist/lib/admin-root-element.d.ts.map +1 -0
- package/dist/lib/admin-root-element.js +14 -0
- package/dist/lib/admin-root-element.js.map +1 -0
- package/dist/lib/editor-seo-content.d.ts +16 -0
- package/dist/lib/editor-seo-content.d.ts.map +1 -0
- package/dist/lib/editor-seo-content.js +139 -0
- package/dist/lib/editor-seo-content.js.map +1 -0
- package/dist/lib/portal-container.d.ts.map +1 -1
- package/dist/lib/portal-container.js +2 -3
- package/dist/lib/portal-container.js.map +1 -1
- package/dist/lib/seo-service.d.ts +91 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +55 -2
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/ApiKeys.js +2 -2
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +12 -14
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +35 -6
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/PageEditor.js +1 -1
- package/dist/views/Posts/PostTypeEditor.js +3 -3
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +4 -2
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTags.js +1 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +6 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +5 -2
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +36 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.js +2 -13
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +2 -2
- package/dist/views/page-editor/SectionsPanel.js +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.js +17 -79
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.js +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +37 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/seo/ContentTab.d.ts.map +1 -1
- package/dist/views/seo/ContentTab.js +7 -1
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/seo/LinksTab.d.ts +4 -0
- package/dist/views/seo/LinksTab.d.ts.map +1 -0
- package/dist/views/seo/LinksTab.js +111 -0
- package/dist/views/seo/LinksTab.js.map +1 -0
- package/dist/views/seo/OverviewTab.js +1 -1
- package/dist/views/seo/OverviewTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +39 -6
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/dist/views/seo/TechnicalTab.js +2 -2
- package/dist/views/settings/AISettingsTab.js +1 -1
- package/dist/views/settings/AiFeaturesCard.js +1 -1
- package/dist/views/settings/AiFeaturesCard.js.map +1 -1
- package/dist/views/settings/AppearanceTab.js +1 -1
- package/dist/views/settings/BrandVoiceCard.js +1 -1
- package/dist/views/settings/EmailSettingsTab.js +2 -2
- package/dist/views/settings/GeneralSettingsTab.js +1 -1
- package/dist/views/settings/SecurityStatusCard.js +1 -1
- package/dist/views/settings/SeoSettingsTab.js +4 -4
- package/dist/views/settings/UpdatesTab.js +2 -2
- package/dist/views/settings/components.js +3 -3
- package/dist/views/settings/components.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +3 -2
- package/src/LoginRoot.tsx +2 -1
- package/src/__tests__/components/theme-provider-accent.test.tsx +84 -0
- package/src/__tests__/views/portal-container.test.tsx +14 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +3 -9
- package/src/__tests__/views/redirects-pane.render.test.tsx +6 -0
- package/src/__tests__/views/seo-content-pane.render.test.tsx +16 -4
- package/src/components/CoauthorResultPopover.tsx +1 -1
- package/src/components/EditorSeoAside.tsx +68 -0
- package/src/components/MediaPickerModal.tsx +1 -1
- package/src/components/SEOConfigPanel.tsx +2 -2
- package/src/components/SchedulePublishDialog.tsx +1 -1
- package/src/components/SharePreviewLinkDialog.tsx +1 -1
- package/src/components/ThemeProvider.tsx +3 -1
- package/src/components/VersionHistory.tsx +1 -1
- package/src/components/seo/EditorSeoScoreBadge.tsx +26 -0
- package/src/components/seo/SeoAccordion.tsx +76 -0
- package/src/components/seo/SeoAnalysisList.tsx +113 -0
- package/src/components/seo/SeoEditorPane.tsx +532 -250
- package/src/components/seo/SeoInsightsMetrics.tsx +109 -0
- package/src/components/seo/SeoIssueFixPanel.tsx +16 -4
- package/src/components/seo/SeoLinkingSection.tsx +151 -0
- package/src/components/seo/primitives.tsx +7 -3
- package/src/components/ui/Card.tsx +5 -5
- package/src/components/ui/InspectorPane.tsx +3 -3
- package/src/components/ui/PanelSection.tsx +53 -0
- package/src/lib/admin-root-element.ts +15 -0
- package/src/lib/editor-seo-content.ts +144 -0
- package/src/lib/portal-container.ts +3 -2
- package/src/lib/seo-service.ts +150 -2
- package/src/views/ApiKeys.tsx +2 -2
- package/src/views/Dashboard.tsx +40 -53
- package/src/views/MediaBrowser.tsx +62 -12
- package/src/views/PageEditor.tsx +1 -1
- package/src/views/Posts/PostTypeEditor.tsx +3 -3
- package/src/views/SEO.tsx +6 -0
- package/src/views/ScriptTags.tsx +2 -2
- package/src/views/Users.tsx +1 -1
- package/src/views/page-editor/EditorTopBar.tsx +27 -0
- package/src/views/page-editor/PageSectionEditor.tsx +58 -3
- package/src/views/page-editor/PageSettingsModal.tsx +1 -45
- package/src/views/page-editor/SectionInspector.tsx +2 -2
- package/src/views/page-editor/SectionsPanel.tsx +1 -1
- package/src/views/post-editor/PostFieldsModal.tsx +19 -138
- package/src/views/post-editor/PostHeaderPanel.tsx +1 -1
- package/src/views/post-editor/PostSectionEditor.tsx +60 -3
- package/src/views/seo/ContentTab.tsx +11 -5
- package/src/views/seo/LinksTab.tsx +462 -0
- package/src/views/seo/OverviewTab.tsx +1 -1
- package/src/views/seo/RedirectsTab.tsx +97 -4
- package/src/views/seo/TechnicalTab.tsx +2 -2
- package/src/views/settings/AISettingsTab.tsx +3 -3
- package/src/views/settings/AiFeaturesCard.tsx +1 -1
- package/src/views/settings/AppearanceTab.tsx +1 -1
- package/src/views/settings/BrandVoiceCard.tsx +2 -2
- package/src/views/settings/EmailSettingsTab.tsx +2 -2
- package/src/views/settings/GeneralSettingsTab.tsx +3 -3
- package/src/views/settings/SecurityStatusCard.tsx +1 -1
- package/src/views/settings/SeoSettingsTab.tsx +6 -6
- package/src/views/settings/UpdatesTab.tsx +3 -3
- package/src/views/settings/components.tsx +3 -3
|
@@ -44,7 +44,7 @@ export function SeoSettingsTab({ onNavigate, role }) {
|
|
|
44
44
|
if (seo.loading) {
|
|
45
45
|
return (_jsxs("div", { className: "space-y-4", "aria-busy": "true", "aria-live": "polite", children: [_jsx("span", { className: "sr-only", children: "Loading SEO settings\u2026" }), [0, 1, 2, 3].map((i) => (_jsxs("div", { className: "border-border bg-card rounded-lg border p-6", children: [_jsx("div", { className: "bg-muted mb-4 h-4 w-40 animate-pulse rounded" }), _jsxs("div", { className: "space-y-3", children: [_jsx("div", { className: "bg-muted h-9 w-full animate-pulse rounded" }), _jsx("div", { className: "bg-muted h-9 w-full animate-pulse rounded" })] })] }, i)))] }));
|
|
46
46
|
}
|
|
47
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsx(SeoWorkspaceCallout, { onNavigate: onNavigate }), seo.loadError && (_jsxs("div", { role: "alert", className: "border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-
|
|
47
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx(SeoWorkspaceCallout, { onNavigate: onNavigate }), seo.loadError && (_jsxs("div", { role: "alert", className: "border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-4 text-sm", children: [_jsx(AlertTriangle, { size: 16, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "flex-1", children: seo.loadError }), _jsx("button", { type: "button", onClick: seo.refetch, className: "border-destructive/40 rounded-md border px-3 py-1 transition-colors hover:opacity-80", children: "Retry" })] })), !canEdit && (_jsxs("div", { className: "border-border bg-muted text-muted-foreground flex items-center gap-2 rounded-lg border p-4 text-sm", children: [_jsx(Info, { size: 16, className: "shrink-0", "aria-hidden": "true" }), "You have read-only access to SEO defaults. Only administrators can make changes."] })), _jsx(SettingsValidationSummary, { issues: seo.issues }), _jsx(MetaDefaultsCard, { seo: seo, canEdit: canEdit }), _jsx(OrganizationCard, { seo: seo, canEdit: canEdit }), _jsx(SearchEngineVerificationCard, { seo: seo, canEdit: canEdit }), _jsx(SitemapDefaultsCard, { seo: seo, canEdit: canEdit, onNavigate: onNavigate }), _jsxs("details", { className: "border-border bg-card group rounded-lg border", children: [_jsxs("summary", { className: "text-foreground flex cursor-pointer list-none items-center justify-between p-4 text-base font-medium", children: [_jsx("span", { children: "Advanced & per-collection SEO" }), _jsx("span", { className: "text-muted-foreground text-sm font-normal group-open:hidden", children: "Show" }), _jsx("span", { className: "text-muted-foreground hidden text-sm font-normal group-open:inline", children: "Hide" })] }), _jsxs("div", { className: "border-border border-t p-4", children: [_jsx("p", { className: "text-muted-foreground mb-4 text-sm", children: "Site name, social handle, per-collection schema, and route toggles. These share the same SEO configuration as the defaults above." }), _jsx(SEOConfigPanel, {})] })] }), seo.saveState === 'error' && seo.saveError && (_jsxs("div", { role: "alert", className: "border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-4 text-sm", children: [_jsx(AlertTriangle, { size: 16, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "flex-1", children: seo.saveError }), _jsx("button", { type: "button", onClick: seo.save, className: "border-destructive/40 rounded-md border px-3 py-1 transition-colors hover:opacity-80", children: "Retry" })] })), _jsx(SettingsSaveBar, { dirty: seo.dirty, canEdit: canEdit, hasErrors: seo.hasErrors, saveState: seo.saveState, onSave: seo.save, onReset: seo.reset })] }));
|
|
48
48
|
}
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
// Full SEO workspace callout
|
|
@@ -78,9 +78,9 @@ function MetaDefaultsCard({ seo, canEdit }) {
|
|
|
78
78
|
}, className: "text-muted-foreground hover:text-destructive inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm transition-colors", children: [_jsx(X, { size: 14, "aria-hidden": "true" }), "Remove"] }))] }), _jsx("p", { className: "text-muted-foreground text-sm", children: "1200\u00D7630 recommended" })] })] })] })] }), _jsx(SeoPreviewCard, { title: previewTitle(form.titleTemplate, seo.siteName), url: seo.siteUrl || 'https://example.com', description: form.defaultMetaDescription.trim() || 'Your default meta description appears here.', image: form.defaultOgImage, siteName: seo.siteName }), _jsx(MediaPickerModal, { open: pickerOpen, onClose: () => setPickerOpen(false), accept: "image/*", onSelect: (url) => setField('defaultOgImage', url), onSelectItem: (item) => setField('defaultSocialImageAssetId', item.id) })] }));
|
|
79
79
|
}
|
|
80
80
|
function SeoPreviewCard({ title, url, description, image, siteName, }) {
|
|
81
|
-
return (_jsxs("div", { className: "border-border mt-4 grid gap-4 border-t pt-4 sm:grid-cols-2", children: [_jsxs("div", { className: "border-border rounded-lg border p-
|
|
81
|
+
return (_jsxs("div", { className: "border-border mt-4 grid gap-4 border-t pt-4 sm:grid-cols-2", children: [_jsxs("div", { className: "border-border rounded-lg border p-4", children: [_jsx("p", { className: "text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase", children: "Search preview" }), _jsx("p", { className: "text-success truncate text-sm", children: url }), _jsx("p", { className: "text-primary truncate text-lg leading-snug", children: title }), _jsx("p", { className: "text-muted-foreground line-clamp-2 text-sm", children: description })] }), _jsxs("div", { className: "border-border overflow-hidden rounded-lg border", children: [_jsx("div", { className: "bg-muted flex aspect-1200/630 items-center justify-center", children: image ? (
|
|
82
82
|
// eslint-disable-next-line @next/next/no-img-element
|
|
83
|
-
_jsx("img", { src: image, alt: "Social share preview", className: "h-full w-full object-cover" })) : (_jsx(ImageIcon, { size: 24, className: "text-muted-foreground", "aria-hidden": "true" })) }), _jsxs("div", { className: "p-
|
|
83
|
+
_jsx("img", { src: image, alt: "Social share preview", className: "h-full w-full object-cover" })) : (_jsx(ImageIcon, { size: 24, className: "text-muted-foreground", "aria-hidden": "true" })) }), _jsxs("div", { className: "p-4", children: [_jsx("p", { className: "text-muted-foreground truncate text-xs uppercase", children: siteName || new URL(url).hostname }), _jsx("p", { className: "text-foreground truncate text-sm font-medium", children: title }), _jsx("p", { className: "text-muted-foreground line-clamp-1 text-xs", children: description })] })] })] }));
|
|
84
84
|
}
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
86
|
// Organization (Schema.org)
|
|
@@ -133,7 +133,7 @@ function SitemapDefaultsCard({ seo, canEdit, onNavigate, }) {
|
|
|
133
133
|
const lastGenerated = sitemap?.lastGeneratedAt
|
|
134
134
|
? new Date(sitemap.lastGeneratedAt).toLocaleString()
|
|
135
135
|
: 'Never';
|
|
136
|
-
return (_jsxs(SettingsCard, { title: "Sitemap", description: "Control XML sitemap generation and whether search engines are pinged on publish.", children: [!siteUrl && (_jsxs("div", { className: "border-warning/30 bg-warning/10 text-warning mb-4 flex items-start gap-2 rounded-md border p-
|
|
136
|
+
return (_jsxs(SettingsCard, { title: "Sitemap", description: "Control XML sitemap generation and whether search engines are pinged on publish.", children: [!siteUrl && (_jsxs("div", { className: "border-warning/30 bg-warning/10 text-warning mb-4 flex items-start gap-2 rounded-md border p-4 text-sm", children: [_jsx(AlertTriangle, { size: 16, className: "mt-0.5 shrink-0", "aria-hidden": "true" }), "Site URL is not set \u2014 sitemap URLs may be incorrect. Set it in Settings \u2192 General."] })), _jsxs("div", { className: "space-y-4", children: [_jsx(SettingsToggleRow, { label: "Auto-generate XML sitemap", description: "Rebuild the sitemap whenever content is published.", checked: form.autoGenerateSitemap, disabled: !canEdit, onChange: requestAutoGenerate }), _jsx(SettingsToggleRow, { label: "Ping search engines on publish", description: "Notify Google and Bing when the sitemap changes.", checked: form.pingOnPublish, disabled: !canEdit || !form.autoGenerateSitemap, onChange: (v) => setField('pingOnPublish', v) })] }), _jsxs("div", { className: "border-border mt-4 space-y-2 border-t pt-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Sitemap URL" }), sitemap?.sitemapUrl ? (_jsxs("a", { href: sitemap.sitemapUrl, target: "_blank", rel: "noreferrer", className: "text-brand inline-flex items-center gap-1 font-medium hover:underline", children: ["View sitemap", _jsx(ExternalLink, { size: 13, "aria-hidden": "true" })] })) : (_jsx("span", { className: "text-foreground font-medium", children: "/sitemap.xml" }))] }), _jsx(SummaryRow, { label: "Last generated", value: lastGenerated }), _jsx(SummaryRow, { label: "Included URLs", value: String(sitemap?.pagesIncluded ?? '—') }), _jsx(SummaryRow, { label: "Excluded URLs", value: String(sitemap?.pagesExcluded ?? '—') }), _jsxs("div", { className: "flex items-center justify-between gap-2 pt-1", children: [_jsxs("button", { type: "button", onClick: regenerateSitemap, disabled: !canEdit || regenerating, className: "border-border text-foreground hover:bg-accent inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-60", children: [regenerating ? (_jsx(Loader2, { size: 14, className: "animate-spin", "aria-hidden": "true" })) : (_jsx(RefreshCw, { size: 14, "aria-hidden": "true" })), "Regenerate now"] }), onNavigate && (_jsx("button", { type: "button", onClick: () => onNavigate('/seo?tab=technical'), className: "text-brand text-sm hover:underline", children: "Manage in SEO Technical \u2192" }))] })] }), _jsx(ConfirmDangerousSettingDialog, { open: pendingDisable, title: "Stop auto-generating the sitemap on production?", description: "Your sitemap may go stale as content changes, which can hurt how quickly search engines discover new pages.", confirmLabel: "Turn Off", onConfirm: () => {
|
|
137
137
|
setField('autoGenerateSitemap', false);
|
|
138
138
|
setPendingDisable(false);
|
|
139
139
|
}, onCancel: () => setPendingDisable(false) })] }));
|
|
@@ -61,14 +61,14 @@ function Notice({ tone, icon, title, children, }) {
|
|
|
61
61
|
const toneClass = tone === 'destructive'
|
|
62
62
|
? 'border-destructive/30 bg-destructive/10 text-destructive'
|
|
63
63
|
: 'border-warning/30 bg-warning/10 text-warning';
|
|
64
|
-
return (_jsxs("div", { className: `mt-3 flex items-start gap-2.5 rounded-md border p-
|
|
64
|
+
return (_jsxs("div", { className: `mt-3 flex items-start gap-2.5 rounded-md border p-4 ${toneClass}`, children: [_jsx("span", { className: "mt-0.5 shrink-0", "aria-hidden": "true", children: icon }), _jsxs("div", { className: "min-w-0 text-sm", children: [_jsx("p", { className: "font-medium", children: title }), children && _jsx("div", { className: "text-foreground/80 mt-1 space-y-1", children: children })] })] }));
|
|
65
65
|
}
|
|
66
66
|
export function UpdatesTab({ role }) {
|
|
67
67
|
const { info, checking, hasChecked, error, applying, prResult, canManage, form, setField, dirty, repoError, hasErrors, saveState, save, reset, check, apply, } = useUpdates({ role });
|
|
68
68
|
const [showToken, setShowToken] = useState(false);
|
|
69
69
|
const severity = info?.updateAvailable ? SEVERITY[info.severity ?? 'patch'] : null;
|
|
70
70
|
const changelogGroups = info?.changelog ? groupByVersion(info.changelog) : [];
|
|
71
|
-
return (_jsxs(_Fragment, { children: [_jsx(SettingsCard, { title: "Actuate CMS", description: "Keep your CMS up to date with the latest features and security patches.", actions: _jsxs("button", { type: "button", onClick: check, disabled: checking, className: "border-border text-foreground hover:bg-accent inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50", children: [_jsx(RefreshCw, { size: 16, className: checking ? 'animate-spin' : '', "aria-hidden": "true" }), checking ? 'Checking…' : 'Check for Updates'] }), children: _jsxs("p", { className: "text-muted-foreground text-sm", children: ["Current version:", ' ', _jsx("span", { className: "text-foreground font-mono font-medium", children: info?.current ?? '…' })] }) }), error && (_jsxs("div", { role: "alert", className: "border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-
|
|
71
|
+
return (_jsxs(_Fragment, { children: [_jsx(SettingsCard, { title: "Actuate CMS", description: "Keep your CMS up to date with the latest features and security patches.", actions: _jsxs("button", { type: "button", onClick: check, disabled: checking, className: "border-border text-foreground hover:bg-accent inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50", children: [_jsx(RefreshCw, { size: 16, className: checking ? 'animate-spin' : '', "aria-hidden": "true" }), checking ? 'Checking…' : 'Check for Updates'] }), children: _jsxs("p", { className: "text-muted-foreground text-sm", children: ["Current version:", ' ', _jsx("span", { className: "text-foreground font-mono font-medium", children: info?.current ?? '…' })] }) }), error && (_jsxs("div", { role: "alert", className: "border-destructive/30 bg-destructive/10 text-destructive flex items-center gap-3 rounded-lg border p-4 text-sm", children: [_jsx(AlertTriangle, { size: 20, className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "flex-1", children: error }), _jsx("button", { type: "button", onClick: check, className: "border-destructive/40 rounded-md border px-3 py-1 transition-colors hover:opacity-80", children: "Retry" })] })), hasChecked && info && !info.updateAvailable && !error && (_jsxs("div", { className: "border-success/30 bg-success/10 flex items-center gap-3 rounded-lg border p-4", children: [_jsx(CheckCircle2, { size: 24, className: "text-success shrink-0", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("h3", { className: "text-foreground font-medium", children: "You're up to date!" }), _jsxs("p", { className: "text-muted-foreground mt-0.5 text-sm", children: ["Actuate CMS ", _jsx("span", { className: "font-mono", children: info.current }), " is the latest version."] })] })] })), info?.updateAvailable && severity && (_jsxs("div", { className: `rounded-lg border p-4 ${severity.banner}`, children: [_jsxs("div", { className: "flex items-start gap-3", children: [info.severity === 'security' ? (_jsx(ShieldAlert, { size: 24, className: `mt-0.5 shrink-0 ${severity.text}`, "aria-hidden": "true" })) : (_jsx(ArrowUpCircle, { size: 24, className: `mt-0.5 shrink-0 ${severity.text}`, "aria-hidden": "true" })), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("h3", { className: `font-medium ${severity.text}`, children: info.severity === 'security' ? 'Security update available' : 'Update available' }), _jsx("span", { className: `inline-flex items-center rounded border px-2 py-0.5 text-xs font-medium ${severity.banner} ${severity.text}`, children: severity.label })] }), _jsxs("p", { className: "text-muted-foreground mt-1 text-sm", children: [_jsx("span", { className: "font-mono", children: info.current }), " \u2192", ' ', _jsx("span", { className: "text-foreground font-mono font-medium", children: info.latest }), info.releaseDate && (_jsxs("span", { className: "ml-2 text-xs opacity-70", children: ["Released ", info.releaseDate] }))] })] })] }), info.hasMigrations && (_jsx(Notice, { tone: "warning", icon: _jsx(Database, { size: 16 }), title: "Database migrations required", children: _jsxs("p", { children: ["Run", ' ', _jsx("code", { className: "bg-muted rounded px-1 font-mono text-xs", children: "npx prisma migrate deploy" }), ' ', "after upgrading. On Vercel this runs automatically at build time."] }) })), info.breakingChanges && info.breakingChanges.length > 0 && (_jsx(Notice, { tone: "destructive", icon: _jsx(AlertTriangle, { size: 16 }), title: "Breaking changes in this release", children: _jsx("ul", { className: "list-disc space-y-1 pl-4", children: info.breakingChanges.map((c, i) => (_jsx("li", { children: c }, i))) }) })), _jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-3", children: [canManage ? (_jsx("button", { type: "button", onClick: apply, disabled: applying, className: "bg-brand text-brand-foreground inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50", children: applying ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 16, className: "animate-spin", "aria-hidden": "true" }), "Creating PR\u2026"] })) : (_jsxs(_Fragment, { children: [_jsx(GitPullRequest, { size: 16, "aria-hidden": "true" }), "Create Update PR"] })) })) : (_jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1.5 text-xs", children: [_jsx(Lock, { size: 14, "aria-hidden": "true" }), "Ask an administrator to apply this update."] })), canManage && (_jsx("span", { className: "text-muted-foreground text-xs", children: "Opens a pull request on your repository" }))] }), info.updateCommand && (_jsxs("div", { className: "border-border bg-card mt-3 rounded-md border p-4", children: [_jsx("p", { className: "text-muted-foreground mb-1 text-xs", children: "Or update manually:" }), _jsx("code", { className: "bg-muted text-foreground block rounded px-2 py-1.5 font-mono text-xs select-all", children: info.updateCommand })] }))] })), info?.updateAvailable && changelogGroups.length > 0 && (_jsx(SettingsCard, { title: "Changelog", description: info.source === 'npm'
|
|
72
72
|
? 'Version history from npm. Connect the update server for full release notes.'
|
|
73
73
|
: "What's new since your current version.", children: _jsx("div", { className: "max-h-80 space-y-4 overflow-y-auto pr-1", children: changelogGroups.map((group) => (_jsxs("div", { className: "border-border border-b pb-3 last:border-0 last:pb-0", children: [_jsxs("div", { className: "mb-1.5 flex items-baseline gap-3", children: [_jsx("span", { className: "text-foreground font-mono text-sm font-medium", children: group.version }), group.date && (_jsx("span", { className: "text-muted-foreground text-xs", children: group.date }))] }), _jsx("ul", { className: "space-y-1.5", children: group.items.map((item, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm", children: [_jsx(ChangeTypeBadge, { type: item.type }), _jsx("span", { className: "text-muted-foreground min-w-0", children: item.summary })] }, i))) })] }, group.version))) }) })), info?.updateAvailable && info.pluginCompatibility && info.pluginCompatibility.length > 0 && (_jsx(SettingsCard, { title: "Plugin compatibility", description: "Latest published versions of the official plugins for this release.", children: _jsx("ul", { className: "grid gap-2 sm:grid-cols-2", children: info.pluginCompatibility.map((plugin) => (_jsxs("li", { className: "border-border flex items-center justify-between gap-3 rounded-md border px-3 py-2 text-sm", children: [_jsxs("span", { className: "text-foreground inline-flex min-w-0 items-center gap-2", children: [_jsx(Puzzle, { size: 14, className: "text-muted-foreground shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "truncate font-mono text-xs", children: plugin.name.replace('@actuate-media/', '') })] }), _jsxs("span", { className: "text-muted-foreground inline-flex shrink-0 items-center gap-1.5", children: [plugin.minVersion && (_jsx("span", { className: "font-mono text-xs", children: plugin.minVersion })), _jsx(CheckCircle2, { size: 14, className: "text-success", "aria-hidden": "true" })] })] }, plugin.name))) }) })), prResult && (_jsx("div", { className: "border-success/30 bg-success/10 rounded-lg border p-4", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(GitPullRequest, { size: 20, className: "text-success mt-0.5 shrink-0", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("h3", { className: "text-foreground font-medium", children: "Pull Request Created" }), _jsxs("p", { className: "text-muted-foreground mt-1 text-sm", children: ["PR #", prResult.prNumber, " has been created on your repository. Review and merge it to apply the update, then run", ' ', _jsx("code", { className: "bg-muted rounded px-1 font-mono text-xs", children: "npx prisma migrate deploy" }), "."] }), _jsxs("a", { href: prResult.prUrl, target: "_blank", rel: "noopener noreferrer", className: "text-brand mt-2 inline-flex items-center gap-1.5 text-sm font-medium hover:underline", children: [_jsx(ExternalLink, { size: 16, "aria-hidden": "true" }), "View Pull Request"] })] })] }) })), canManage && (_jsxs(SettingsCard, { title: "GitHub Integration", description: "Connect your repository to enable one-click update PRs. Credentials are encrypted at rest (AES-256-GCM).", children: [_jsxs("div", { className: "space-y-4", children: [_jsx(SettingsControlRow, { label: "Repository", htmlFor: "updates-gh-repo", align: "start", description: _jsxs(_Fragment, { children: ["e.g.", ' ', _jsx("code", { className: "bg-muted rounded px-1 font-mono", children: "actuate-media/my-client-site" })] }), control: _jsxs("div", { className: "w-72 max-w-full", children: [_jsx("input", { id: "updates-gh-repo", type: "text", value: form.githubRepo, onChange: (e) => setField('githubRepo', e.target.value), placeholder: "owner/repo", "aria-invalid": Boolean(repoError), "aria-describedby": repoError ? 'updates-gh-repo-error' : undefined, className: ghInputClass }), repoError && (_jsx("p", { id: "updates-gh-repo-error", className: "text-destructive mt-1 text-xs", children: repoError }))] }) }), _jsx(SettingsControlRow, { label: _jsxs("span", { className: "inline-flex items-center gap-2", children: ["GitHub token", info?.hasGithubToken && !form.githubToken && (_jsx("span", { className: "text-success text-xs font-normal", children: "Saved" }))] }), htmlFor: "updates-gh-token", align: "start", description: _jsxs(_Fragment, { children: ["Needs ", _jsx("code", { className: "bg-muted rounded px-1 font-mono", children: "repo" }), " scope. Create one at", ' ', _jsx("a", { href: "https://github.com/settings/tokens", target: "_blank", rel: "noopener noreferrer", className: "text-brand hover:underline", children: "github.com/settings/tokens" }), "."] }), control: _jsxs("div", { className: "relative w-72 max-w-full", children: [_jsx("input", { id: "updates-gh-token", type: showToken ? 'text' : 'password', value: form.githubToken, onChange: (e) => setField('githubToken', e.target.value), placeholder: info?.hasGithubToken ? '••••••••••••••••' : 'ghp_… or github_pat_…', className: `${ghInputClass} pr-10` }), _jsx("button", { type: "button", onClick: () => setShowToken((s) => !s), "aria-label": showToken ? 'Hide token' : 'Show token', className: "text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2 p-1", children: showToken ? (_jsx(EyeOff, { size: 16, "aria-hidden": "true" })) : (_jsx(Eye, { size: 16, "aria-hidden": "true" })) })] }) })] }), _jsx(SettingsSaveBar, { dirty: dirty, canEdit: canManage, hasErrors: hasErrors, saveState: saveState, onSave: save, onReset: reset })] })), _jsx(SettingsCard, { title: "How Updates Work", children: _jsx("ol", { className: "text-muted-foreground space-y-1.5 text-sm", children: [
|
|
74
74
|
'Click “Check for Updates” to see if a new version is available.',
|
|
@@ -17,7 +17,7 @@ import { adminPortalContainer } from '../../lib/portal-container.js';
|
|
|
17
17
|
*/
|
|
18
18
|
export function SettingsCard({ title, description, actions, children, className, headingLevel: Heading = 'h3', }) {
|
|
19
19
|
const headingId = useId();
|
|
20
|
-
return (_jsxs(Card, { padding: "none", "aria-labelledby": headingId, role: "region", className: className, children: [_jsxs(CardHeader, { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(Heading, { id: headingId, className: "text-[length:var(--fs-card)] font-semibold text-[var(--txt)]", children: title }), description && _jsx(CardDescription, { children: description })] }), actions && _jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })] }), _jsx(CardContent, { className: "
|
|
20
|
+
return (_jsxs(Card, { padding: "none", "aria-labelledby": headingId, role: "region", className: className, children: [_jsxs(CardHeader, { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(Heading, { id: headingId, className: "text-[length:var(--fs-card)] font-semibold text-[var(--txt)]", children: title }), description && _jsx(CardDescription, { children: description })] }), actions && _jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })] }), _jsx(CardContent, { className: "text-[var(--txt)]", children: children })] }));
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* A sub-section inside a {@link SettingsCard}: a top divider plus a
|
|
@@ -25,7 +25,7 @@ export function SettingsCard({ title, description, actions, children, className,
|
|
|
25
25
|
* within a single card (e.g. "Admin branding" under "Branding").
|
|
26
26
|
*/
|
|
27
27
|
export function SettingsSubsection({ title, description, children, className, }) {
|
|
28
|
-
return (_jsxs("div", { className: `border-border border-t pt-4 ${className ?? ''}`, children: [_jsx("div", { className: "text-foreground text-base font-medium", children: title }), description && _jsx("p", { className: "text-muted-foreground mt-0.5 text-sm", children: description }), children] }));
|
|
28
|
+
return (_jsxs("div", { className: `border-border border-t pt-4 ${className ?? ''}`, children: [_jsx("div", { className: "text-foreground text-base font-medium", children: title }), description && _jsx("p", { className: "text-muted-foreground mt-0.5 text-sm", children: description }), children ? _jsx("div", { className: "mt-4", children: children }) : null] }));
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* A single setting row: an optional leading visual plus a label and
|
|
@@ -63,7 +63,7 @@ export function SettingsValidationSummary({ issues }) {
|
|
|
63
63
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
64
64
|
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
65
65
|
const isError = errors.length > 0;
|
|
66
|
-
return (_jsxs("div", { role: "alert", "aria-live": "polite", className: `flex items-start gap-3 rounded-lg border p-
|
|
66
|
+
return (_jsxs("div", { role: "alert", "aria-live": "polite", className: `flex items-start gap-3 rounded-lg border p-4 text-sm ${isError
|
|
67
67
|
? 'border-destructive/30 bg-destructive/10 text-destructive'
|
|
68
68
|
: 'border-warning/30 bg-warning/10 text-warning'}`, children: [_jsx(AlertTriangle, { size: 16, className: "mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { className: "space-y-1", children: [...errors, ...warnings].map((issue, i) => (_jsx("p", { children: issue.message }, `${issue.field}-${i}`))) })] }));
|
|
69
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.js","sourceRoot":"","sources":["../../../src/views/settings/components.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAClE,OAAO,EAAE,KAAK,EAAkB,MAAM,OAAO,CAAA;AAC7C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AAGpE;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,WAAW,EACX,OAAO,EACP,QAAQ,EACR,SAAS,EACT,YAAY,EAAE,OAAO,GAAG,IAAI,GAU7B;IACC,MAAM,SAAS,GAAG,KAAK,EAAE,CAAA;IACzB,OAAO,CACL,MAAC,IAAI,IAAC,OAAO,EAAC,MAAM,qBAAkB,SAAS,EAAE,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAE,SAAS,aACjF,MAAC,UAAU,IAAC,SAAS,EAAC,wCAAwC,aAC5D,eAAK,SAAS,EAAC,SAAS,aACtB,KAAC,OAAO,IACN,EAAE,EAAE,SAAS,EACb,SAAS,EAAC,8DAA8D,YAEvE,KAAK,GACE,EACT,WAAW,IAAI,KAAC,eAAe,cAAE,WAAW,GAAmB,IAC5D,EACL,OAAO,IAAI,cAAK,SAAS,EAAC,kCAAkC,YAAE,OAAO,GAAO,IAClE,EACb,KAAC,WAAW,IAAC,SAAS,EAAC,
|
|
1
|
+
{"version":3,"file":"components.js","sourceRoot":"","sources":["../../../src/views/settings/components.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cms-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/actuate-media/actuatecms.git",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"tailwindcss": "^4.0.0",
|
|
96
96
|
"typescript": "^5.7.0",
|
|
97
97
|
"vitest": "^4.1.0",
|
|
98
|
-
"@actuate-media/cms-core": "0.
|
|
98
|
+
"@actuate-media/cms-core": "0.90.1",
|
|
99
99
|
"@actuate-media/component-blocks": "0.2.1"
|
|
100
100
|
},
|
|
101
101
|
"scripts": {
|
package/src/AdminRoot.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import * as Dialog from '@radix-ui/react-dialog'
|
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { Layout } from './layout/Layout.js'
|
|
7
7
|
import { adminPortalContainer } from './lib/portal-container.js'
|
|
8
|
+
import { ADMIN_ROOT_ATTR, getAdminRootElement } from './lib/admin-root-element.js'
|
|
8
9
|
import { useAdminRouter, parseRouteQuery } from './router/index.js'
|
|
9
10
|
import { cmsApi, ensureCsrfToken } from './lib/api.js'
|
|
10
11
|
import { setDocumentFavicon, BRAND_UPDATED_EVENT } from './lib/favicon.js'
|
|
@@ -856,7 +857,7 @@ function useBranding(config: any) {
|
|
|
856
857
|
}
|
|
857
858
|
|
|
858
859
|
// --- Primary color ---
|
|
859
|
-
const adminEl =
|
|
860
|
+
const adminEl = getAdminRootElement()
|
|
860
861
|
if (branding?.primaryColor && adminEl) {
|
|
861
862
|
adminEl.style.setProperty('--primary', branding.primaryColor)
|
|
862
863
|
adminEl.style.setProperty('--sidebar-primary', branding.primaryColor)
|
|
@@ -938,7 +939,7 @@ export function AdminRoot(props: AdminRootProps) {
|
|
|
938
939
|
}, [props.sentryDsn])
|
|
939
940
|
|
|
940
941
|
return (
|
|
941
|
-
<div style={ISOLATION_STYLE} className="actuate-admin">
|
|
942
|
+
<div style={ISOLATION_STYLE} className="actuate-admin" {...{ [ADMIN_ROOT_ATTR]: '' }}>
|
|
942
943
|
<ThemeProvider defaultDarkMode={defaultDarkMode}>
|
|
943
944
|
<LocaleProvider config={props.config}>
|
|
944
945
|
<AdminSectionRenderersProvider value={props.sectionRenderers ?? EMPTY_RENDERERS}>
|
package/src/LoginRoot.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useEffect } from 'react'
|
|
|
4
4
|
import { useAdminRouter } from './router/index.js'
|
|
5
5
|
import { ThemeProvider } from './components/ThemeProvider.js'
|
|
6
6
|
import { ISOLATION_STYLE, useRobotsNoIndex } from './lib/root-chrome.js'
|
|
7
|
+
import { ADMIN_ROOT_ATTR } from './lib/admin-root-element.js'
|
|
7
8
|
import { Login, type CaptchaConfig } from './views/Login.js'
|
|
8
9
|
import { SetupWizard } from './views/SetupWizard.js'
|
|
9
10
|
import { ForgotPassword } from './views/ForgotPassword.js'
|
|
@@ -79,7 +80,7 @@ export function LoginRoot({
|
|
|
79
80
|
useRobotsNoIndex()
|
|
80
81
|
|
|
81
82
|
return (
|
|
82
|
-
<div style={ISOLATION_STYLE} className="actuate-admin">
|
|
83
|
+
<div style={ISOLATION_STYLE} className="actuate-admin" {...{ [ADMIN_ROOT_ATTR]: '' }}>
|
|
83
84
|
<ThemeProvider defaultDarkMode={defaultDarkMode}>
|
|
84
85
|
<LoginShell
|
|
85
86
|
config={config}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { cleanup, render, waitFor } from '@testing-library/react'
|
|
4
|
+
import { accentTokensToCssVars, generateAdminThemeTokens } from '@actuate-media/cms-core/appearance'
|
|
5
|
+
|
|
6
|
+
import { ThemeProvider } from '../../components/ThemeProvider.js'
|
|
7
|
+
import { ADMIN_ROOT_ATTR, getAdminRootElement } from '../../lib/admin-root-element.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regression: accent overrides must land on the inner AdminRoot shell, not a
|
|
11
|
+
* consumer wrapper `.actuate-admin` that also defines static --acc tokens.
|
|
12
|
+
*/
|
|
13
|
+
afterEach(cleanup)
|
|
14
|
+
|
|
15
|
+
const storage = new Map<string, string>()
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
storage.clear()
|
|
19
|
+
vi.stubGlobal('localStorage', {
|
|
20
|
+
getItem: (key: string) => storage.get(key) ?? null,
|
|
21
|
+
setItem: (key: string, value: string) => storage.set(key, value),
|
|
22
|
+
removeItem: (key: string) => storage.delete(key),
|
|
23
|
+
clear: () => storage.clear(),
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function mountNestedAdminShell() {
|
|
28
|
+
const outer = document.createElement('div')
|
|
29
|
+
outer.className = 'actuate-admin'
|
|
30
|
+
document.body.appendChild(outer)
|
|
31
|
+
|
|
32
|
+
const inner = document.createElement('div')
|
|
33
|
+
inner.className = 'actuate-admin'
|
|
34
|
+
inner.setAttribute(ADMIN_ROOT_ATTR, '')
|
|
35
|
+
outer.appendChild(inner)
|
|
36
|
+
|
|
37
|
+
return { outer, inner }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('getAdminRootElement', () => {
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
document.body.innerHTML = ''
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('prefers the marked admin shell over an outer wrapper', () => {
|
|
46
|
+
const { outer, inner } = mountNestedAdminShell()
|
|
47
|
+
expect(getAdminRootElement()).toBe(inner)
|
|
48
|
+
expect(getAdminRootElement()).not.toBe(outer)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('falls back to .actuate-admin when no marked root exists', () => {
|
|
52
|
+
const root = document.createElement('div')
|
|
53
|
+
root.className = 'actuate-admin'
|
|
54
|
+
document.body.appendChild(root)
|
|
55
|
+
expect(getAdminRootElement()).toBe(root)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('ThemeProvider accent application', () => {
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
document.body.innerHTML = ''
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('applies accent tokens to the marked inner admin root when nested', async () => {
|
|
65
|
+
const { outer, inner } = mountNestedAdminShell()
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<ThemeProvider>
|
|
69
|
+
<span>child</span>
|
|
70
|
+
</ThemeProvider>,
|
|
71
|
+
{ container: inner },
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const expected = accentTokensToCssVars(
|
|
75
|
+
generateAdminThemeTokens({ adminAccentColor: 'purple' }, 'light'),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(inner.style.getPropertyValue('--acc')).toBe(expected['--acc']!)
|
|
80
|
+
})
|
|
81
|
+
expect(inner.style.getPropertyValue('--primary')).toBe(expected['--primary']!)
|
|
82
|
+
expect(outer.style.getPropertyValue('--acc')).toBe('')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
|
|
3
3
|
import { cleanup, render, screen } from '@testing-library/react'
|
|
4
4
|
import { ConfirmDangerousSettingDialog } from '../../views/settings/components.js'
|
|
5
5
|
import { adminPortalContainer } from '../../lib/portal-container.js'
|
|
6
|
+
import { ADMIN_ROOT_ATTR } from '../../lib/admin-root-element.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Regression: theme tokens only exist under `.actuate-admin` (theme.css), so
|
|
@@ -24,11 +25,24 @@ describe('adminPortalContainer', () => {
|
|
|
24
25
|
it('resolves the admin root element', () => {
|
|
25
26
|
const root = document.createElement('div')
|
|
26
27
|
root.className = 'actuate-admin'
|
|
28
|
+
root.setAttribute(ADMIN_ROOT_ATTR, '')
|
|
27
29
|
document.body.appendChild(root)
|
|
28
30
|
expect(adminPortalContainer()).toBe(root)
|
|
29
31
|
root.remove()
|
|
30
32
|
})
|
|
31
33
|
|
|
34
|
+
it('prefers the marked shell when nested inside another .actuate-admin', () => {
|
|
35
|
+
const outer = document.createElement('div')
|
|
36
|
+
outer.className = 'actuate-admin'
|
|
37
|
+
const inner = document.createElement('div')
|
|
38
|
+
inner.className = 'actuate-admin'
|
|
39
|
+
inner.setAttribute(ADMIN_ROOT_ATTR, '')
|
|
40
|
+
outer.appendChild(inner)
|
|
41
|
+
document.body.appendChild(outer)
|
|
42
|
+
expect(adminPortalContainer()).toBe(inner)
|
|
43
|
+
outer.remove()
|
|
44
|
+
})
|
|
45
|
+
|
|
32
46
|
it('returns undefined when no admin root is mounted', () => {
|
|
33
47
|
document.querySelectorAll('.actuate-admin').forEach((el) => el.remove())
|
|
34
48
|
expect(adminPortalContainer()).toBeUndefined()
|
|
@@ -28,10 +28,6 @@ vi.mock('../../lib/coauthor-client.js', () => ({
|
|
|
28
28
|
})),
|
|
29
29
|
}))
|
|
30
30
|
|
|
31
|
-
vi.mock('../../lib/seo-service.js', () => ({
|
|
32
|
-
generateSeoField: vi.fn(async () => ({ text: 'AI SEO title' })),
|
|
33
|
-
}))
|
|
34
|
-
|
|
35
31
|
const { PostFieldsModal } = await import('../../views/post-editor/PostFieldsModal.js')
|
|
36
32
|
type ModalProps = Parameters<typeof PostFieldsModal>[0]
|
|
37
33
|
|
|
@@ -152,13 +148,11 @@ describe('PostFieldsModal', () => {
|
|
|
152
148
|
expect((screen.getByLabelText(/^Slug/) as HTMLInputElement).value).toBe('custom-slug')
|
|
153
149
|
})
|
|
154
150
|
|
|
155
|
-
it('exposes AI generate
|
|
151
|
+
it('exposes an AI generate control for the excerpt field', () => {
|
|
156
152
|
render(<PostFieldsModal open post={basePost()} canEdit onClose={() => {}} onSave={() => {}} />)
|
|
157
153
|
expect(screen.getByRole('button', { name: 'Generate excerpt from content' })).toBeTruthy()
|
|
158
|
-
expect(screen.
|
|
159
|
-
expect(
|
|
160
|
-
screen.getByRole('button', { name: 'Generate SEO description from content' }),
|
|
161
|
-
).toBeTruthy()
|
|
154
|
+
expect(screen.queryByLabelText('SEO title')).toBeNull()
|
|
155
|
+
expect(screen.queryByLabelText('SEO description')).toBeNull()
|
|
162
156
|
})
|
|
163
157
|
|
|
164
158
|
it('prompts before discarding when Cancel is clicked with unsaved edits', () => {
|
|
@@ -55,6 +55,12 @@ vi.mock('../../lib/seo-service.js', () => ({
|
|
|
55
55
|
dismissRedirectSuggestion: vi.fn(async () => ({})),
|
|
56
56
|
exportRedirectsCsv: vi.fn(async () => ({ csv: '' })),
|
|
57
57
|
importRedirectsCsv: vi.fn(async () => ({ imported: 0 })),
|
|
58
|
+
fetchRedirectRecoverySettings: vi.fn(async () => ({
|
|
59
|
+
aiRerank: false,
|
|
60
|
+
autoApply: false,
|
|
61
|
+
autoApplyMinConfidence: 0.85,
|
|
62
|
+
})),
|
|
63
|
+
saveRedirectRecoverySettings: vi.fn(async () => ({})),
|
|
58
64
|
}))
|
|
59
65
|
|
|
60
66
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn() } }))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Content SEO tab with the inline editor pane (design split-pane pattern —
|
|
@@ -18,6 +18,9 @@ function makeRecord(over: Record<string, unknown> = {}) {
|
|
|
18
18
|
url: 'https://example.com/',
|
|
19
19
|
canonicalUrl: null,
|
|
20
20
|
metaTitle: 'Home — Example',
|
|
21
|
+
metaTitlePage: 'Home',
|
|
22
|
+
siteName: 'Example',
|
|
23
|
+
titleTemplate: '%page% — %site%',
|
|
21
24
|
metaDescription: 'Welcome to Example.',
|
|
22
25
|
focusKeyword: null,
|
|
23
26
|
keyTakeaway: null,
|
|
@@ -54,11 +57,18 @@ const fetchSeoContentRecord = vi.fn(async (entityType: string, entityId: string)
|
|
|
54
57
|
})
|
|
55
58
|
const updateSeoFields = vi.fn(async (_t: string, _i: string, _p: unknown) => ({}))
|
|
56
59
|
|
|
60
|
+
vi.mock('@actuate-media/cms-core/client', async (importOriginal) => {
|
|
61
|
+
const actual = await importOriginal<typeof import('@actuate-media/cms-core/client')>()
|
|
62
|
+
return { ...actual }
|
|
63
|
+
})
|
|
64
|
+
|
|
57
65
|
vi.mock('../../lib/seo-service.js', () => ({
|
|
58
66
|
fetchSeoContentRecords: () => fetchSeoContentRecords(),
|
|
59
67
|
fetchSeoContentRecord: (t: string, i: string) => fetchSeoContentRecord(t, i),
|
|
68
|
+
fetchDocumentSeoAnalysis: vi.fn(async () => ({ score: 72, checks: [] })),
|
|
60
69
|
updateSeoFields: (t: string, i: string, p: unknown) => updateSeoFields(t, i, p),
|
|
61
70
|
generateSeoField: vi.fn(async () => ({ unavailable: true })),
|
|
71
|
+
fetchLinkSuggestions: vi.fn(async () => ({ data: { suggestions: [] } })),
|
|
62
72
|
SEO_SCHEMA_TYPES: ['Article', 'WebPage'],
|
|
63
73
|
applyContentSeoQuery: (
|
|
64
74
|
records: Array<{ entityType: string; title: string }>,
|
|
@@ -99,8 +109,10 @@ describe('Content SEO inline editor pane', () => {
|
|
|
99
109
|
expect(screen.getByRole('region', { name: 'SEO editor' })).toBeTruthy()
|
|
100
110
|
expect(rowButton.getAttribute('aria-expanded')).toBe('true')
|
|
101
111
|
await waitFor(() => expect(fetchSeoContentRecord).toHaveBeenCalledWith('page', 'p1'))
|
|
102
|
-
//
|
|
103
|
-
expect(await screen.findByDisplayValue('Home
|
|
112
|
+
// Form loads the stored page portion; preview applies the site title template.
|
|
113
|
+
expect(await screen.findByDisplayValue('Home')).toBeTruthy()
|
|
114
|
+
const editor = screen.getByRole('region', { name: 'SEO editor' })
|
|
115
|
+
expect(within(editor).getAllByText('Home — Example').length).toBeGreaterThan(0)
|
|
104
116
|
})
|
|
105
117
|
|
|
106
118
|
it('toggles the pane closed when the same row is clicked again', async () => {
|
|
@@ -118,7 +130,7 @@ describe('Content SEO inline editor pane', () => {
|
|
|
118
130
|
render(<ContentTab />)
|
|
119
131
|
fireEvent.click(await screen.findByRole('button', { name: /Home/ }))
|
|
120
132
|
|
|
121
|
-
const titleInput = await screen.findByDisplayValue('Home
|
|
133
|
+
const titleInput = await screen.findByDisplayValue('Home')
|
|
122
134
|
fireEvent.change(titleInput, { target: { value: 'New Meta Title' } })
|
|
123
135
|
fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }))
|
|
124
136
|
|
|
@@ -120,7 +120,7 @@ export function CoauthorResultPopover({
|
|
|
120
120
|
onClick={(e) => e.stopPropagation()}
|
|
121
121
|
className="bg-card w-full max-w-2xl rounded-lg shadow-2xl outline-none"
|
|
122
122
|
>
|
|
123
|
-
<div className="border-border flex items-center justify-between border-b px-
|
|
123
|
+
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
|
124
124
|
<div className="text-foreground flex items-center gap-2 text-sm font-medium">
|
|
125
125
|
<Sparkles className="text-primary h-4 w-4" aria-hidden />
|
|
126
126
|
<span>AI {coauthorActionLabel(action)}</span>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo } from 'react'
|
|
4
|
+
import { SeoEditorPane } from './seo/SeoEditorPane.js'
|
|
5
|
+
import { ResizableAside } from './ui/ResizableAside.js'
|
|
6
|
+
import { buildEditorContentHtml } from '../lib/editor-seo-content.js'
|
|
7
|
+
import type { PageSection } from '@actuate-media/cms-core/page-sections'
|
|
8
|
+
|
|
9
|
+
const INSPECTOR_SHELL = 'bg-card border-border flex h-full flex-col border-l'
|
|
10
|
+
|
|
11
|
+
export interface EditorSeoAsideProps {
|
|
12
|
+
entityType: 'page' | 'post'
|
|
13
|
+
/** Saved document id — when absent, draft mode with live analysis. */
|
|
14
|
+
entityId: string | null
|
|
15
|
+
title: string
|
|
16
|
+
slug: string
|
|
17
|
+
seoTitle: string
|
|
18
|
+
seoDescription: string
|
|
19
|
+
/** Post body HTML and/or section content for live SEO analysis. */
|
|
20
|
+
sections?: PageSection[]
|
|
21
|
+
bodyHtml?: string
|
|
22
|
+
canEdit: boolean
|
|
23
|
+
onClose: () => void
|
|
24
|
+
onDraftChange?: (patch: { seoTitle?: string; seoDescription?: string }) => void
|
|
25
|
+
onSaved?: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Right-side SEO panel for the Page / Post section editors. */
|
|
29
|
+
export function EditorSeoAside({
|
|
30
|
+
entityType,
|
|
31
|
+
entityId,
|
|
32
|
+
slug,
|
|
33
|
+
seoTitle,
|
|
34
|
+
seoDescription,
|
|
35
|
+
sections = [],
|
|
36
|
+
bodyHtml = '',
|
|
37
|
+
onClose,
|
|
38
|
+
onDraftChange,
|
|
39
|
+
onSaved,
|
|
40
|
+
}: EditorSeoAsideProps) {
|
|
41
|
+
const contentHtml = useMemo(
|
|
42
|
+
() => buildEditorContentHtml(sections, bodyHtml),
|
|
43
|
+
[sections, bodyHtml],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
function onKey(e: KeyboardEvent) {
|
|
48
|
+
if (e.key === 'Escape') onClose()
|
|
49
|
+
}
|
|
50
|
+
document.addEventListener('keydown', onKey)
|
|
51
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
52
|
+
}, [onClose])
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ResizableAside role="dialog" aria-label="SEO settings" className={INSPECTOR_SHELL}>
|
|
56
|
+
<SeoEditorPane
|
|
57
|
+
entityType={entityType}
|
|
58
|
+
entityId={entityId}
|
|
59
|
+
slug={slug}
|
|
60
|
+
contentHtml={contentHtml}
|
|
61
|
+
draftMeta={{ seoTitle, seoDescription }}
|
|
62
|
+
onDraftChange={onDraftChange}
|
|
63
|
+
onClose={onClose}
|
|
64
|
+
onSaved={onSaved}
|
|
65
|
+
/>
|
|
66
|
+
</ResizableAside>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -138,7 +138,7 @@ export function MediaPickerModal({
|
|
|
138
138
|
aria-describedby={undefined}
|
|
139
139
|
className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-70 mx-4 flex max-h-[80vh] w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 flex-col rounded-xl border shadow-2xl focus:outline-none"
|
|
140
140
|
>
|
|
141
|
-
<div className="border-border flex items-center justify-between border-b px-
|
|
141
|
+
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
|
142
142
|
<Dialog.Title className="text-foreground text-lg font-medium">
|
|
143
143
|
Insert Image
|
|
144
144
|
</Dialog.Title>
|
|
@@ -242,7 +242,7 @@ export function SEOConfigPanel() {
|
|
|
242
242
|
|
|
243
243
|
{/* Site-wide section */}
|
|
244
244
|
<section className="border-border bg-card rounded-lg border">
|
|
245
|
-
<header className="border-border flex items-center gap-2 border-b px-
|
|
245
|
+
<header className="border-border flex items-center gap-2 border-b px-6 py-4">
|
|
246
246
|
<Globe className="text-primary h-4 w-4" />
|
|
247
247
|
<h3 className="text-foreground text-sm font-medium">Site-wide defaults</h3>
|
|
248
248
|
</header>
|
|
@@ -333,7 +333,7 @@ export function SEOConfigPanel() {
|
|
|
333
333
|
|
|
334
334
|
{/* Per-collection section */}
|
|
335
335
|
<section className="border-border bg-card rounded-lg border">
|
|
336
|
-
<header className="border-border flex items-center gap-2 border-b px-
|
|
336
|
+
<header className="border-border flex items-center gap-2 border-b px-6 py-4">
|
|
337
337
|
<FileText className="text-primary h-4 w-4" />
|
|
338
338
|
<h3 className="text-foreground text-sm font-medium">Per-collection defaults</h3>
|
|
339
339
|
<span className="text-muted-foreground text-xs">
|
|
@@ -142,7 +142,7 @@ export function SchedulePublishDialog({
|
|
|
142
142
|
<Dialog.Portal container={adminPortalContainer()}>
|
|
143
143
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
|
|
144
144
|
<Dialog.Content className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none">
|
|
145
|
-
<div className="border-border flex items-center justify-between border-b px-
|
|
145
|
+
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
|
146
146
|
<div className="flex items-center gap-2">
|
|
147
147
|
<Calendar className="text-muted-foreground h-5 w-5" aria-hidden />
|
|
148
148
|
<Dialog.Title className="text-foreground text-lg font-medium">
|
|
@@ -129,7 +129,7 @@ export function SharePreviewLinkDialog({
|
|
|
129
129
|
<Dialog.Portal container={adminPortalContainer()}>
|
|
130
130
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
|
|
131
131
|
<Dialog.Content className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none">
|
|
132
|
-
<div className="border-border flex items-center justify-between border-b px-
|
|
132
|
+
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
|
133
133
|
<div className="flex items-center gap-2">
|
|
134
134
|
<Share2 className="text-muted-foreground h-5 w-5" aria-hidden />
|
|
135
135
|
<Dialog.Title className="text-foreground text-lg font-medium">
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from '@actuate-media/cms-core/appearance'
|
|
9
9
|
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
|
|
10
10
|
|
|
11
|
+
import { getAdminRootElement } from '../lib/admin-root-element.js'
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Admin appearance engine.
|
|
13
15
|
*
|
|
@@ -119,7 +121,7 @@ const KEY_MOTION = 'actuate-reduce-motion'
|
|
|
119
121
|
const KEY_BRAND = 'actuate-admin-brand'
|
|
120
122
|
|
|
121
123
|
function adminRootEl(): HTMLElement | null {
|
|
122
|
-
return
|
|
124
|
+
return getAdminRootElement()
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
function applyColorMode(resolved: 'light' | 'dark') {
|