@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.
Files changed (209) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +3 -2
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/LoginRoot.d.ts.map +1 -1
  6. package/dist/LoginRoot.js +2 -1
  7. package/dist/LoginRoot.js.map +1 -1
  8. package/dist/__tests__/components/theme-provider-accent.test.d.ts +2 -0
  9. package/dist/__tests__/components/theme-provider-accent.test.d.ts.map +1 -0
  10. package/dist/__tests__/components/theme-provider-accent.test.js +64 -0
  11. package/dist/__tests__/components/theme-provider-accent.test.js.map +1 -0
  12. package/dist/__tests__/views/portal-container.test.js +13 -0
  13. package/dist/__tests__/views/portal-container.test.js.map +1 -1
  14. package/dist/__tests__/views/post-fields-modal.render.test.js +3 -6
  15. package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
  16. package/dist/__tests__/views/redirects-pane.render.test.js +6 -0
  17. package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
  18. package/dist/__tests__/views/seo-content-pane.render.test.js +15 -4
  19. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
  20. package/dist/actuate-admin.css +1 -1
  21. package/dist/components/CoauthorResultPopover.js +1 -1
  22. package/dist/components/EditorSeoAside.d.ts +23 -0
  23. package/dist/components/EditorSeoAside.d.ts.map +1 -0
  24. package/dist/components/EditorSeoAside.js +21 -0
  25. package/dist/components/EditorSeoAside.js.map +1 -0
  26. package/dist/components/MediaPickerModal.js +1 -1
  27. package/dist/components/SEOConfigPanel.js +1 -1
  28. package/dist/components/SchedulePublishDialog.js +1 -1
  29. package/dist/components/SharePreviewLinkDialog.js +1 -1
  30. package/dist/components/ThemeProvider.d.ts.map +1 -1
  31. package/dist/components/ThemeProvider.js +2 -1
  32. package/dist/components/ThemeProvider.js.map +1 -1
  33. package/dist/components/VersionHistory.js +1 -1
  34. package/dist/components/seo/EditorSeoScoreBadge.d.ts +7 -0
  35. package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -0
  36. package/dist/components/seo/EditorSeoScoreBadge.js +9 -0
  37. package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -0
  38. package/dist/components/seo/SeoAccordion.d.ts +16 -0
  39. package/dist/components/seo/SeoAccordion.d.ts.map +1 -0
  40. package/dist/components/seo/SeoAccordion.js +21 -0
  41. package/dist/components/seo/SeoAccordion.js.map +1 -0
  42. package/dist/components/seo/SeoAnalysisList.d.ts +7 -0
  43. package/dist/components/seo/SeoAnalysisList.d.ts.map +1 -0
  44. package/dist/components/seo/SeoAnalysisList.js +32 -0
  45. package/dist/components/seo/SeoAnalysisList.js.map +1 -0
  46. package/dist/components/seo/SeoEditorPane.d.ts +15 -4
  47. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  48. package/dist/components/seo/SeoEditorPane.js +155 -35
  49. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  50. package/dist/components/seo/SeoInsightsMetrics.d.ts +6 -0
  51. package/dist/components/seo/SeoInsightsMetrics.d.ts.map +1 -0
  52. package/dist/components/seo/SeoInsightsMetrics.js +16 -0
  53. package/dist/components/seo/SeoInsightsMetrics.js.map +1 -0
  54. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  55. package/dist/components/seo/SeoIssueFixPanel.js +13 -3
  56. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  57. package/dist/components/seo/SeoLinkingSection.d.ts +9 -0
  58. package/dist/components/seo/SeoLinkingSection.d.ts.map +1 -0
  59. package/dist/components/seo/SeoLinkingSection.js +44 -0
  60. package/dist/components/seo/SeoLinkingSection.js.map +1 -0
  61. package/dist/components/seo/primitives.d.ts.map +1 -1
  62. package/dist/components/seo/primitives.js +3 -3
  63. package/dist/components/seo/primitives.js.map +1 -1
  64. package/dist/components/ui/Card.js +5 -5
  65. package/dist/components/ui/Card.js.map +1 -1
  66. package/dist/components/ui/InspectorPane.js +3 -3
  67. package/dist/components/ui/InspectorPane.js.map +1 -1
  68. package/dist/components/ui/PanelSection.d.ts +16 -0
  69. package/dist/components/ui/PanelSection.d.ts.map +1 -0
  70. package/dist/components/ui/PanelSection.js +15 -0
  71. package/dist/components/ui/PanelSection.js.map +1 -0
  72. package/dist/lib/admin-root-element.d.ts +9 -0
  73. package/dist/lib/admin-root-element.d.ts.map +1 -0
  74. package/dist/lib/admin-root-element.js +14 -0
  75. package/dist/lib/admin-root-element.js.map +1 -0
  76. package/dist/lib/editor-seo-content.d.ts +16 -0
  77. package/dist/lib/editor-seo-content.d.ts.map +1 -0
  78. package/dist/lib/editor-seo-content.js +139 -0
  79. package/dist/lib/editor-seo-content.js.map +1 -0
  80. package/dist/lib/portal-container.d.ts.map +1 -1
  81. package/dist/lib/portal-container.js +2 -3
  82. package/dist/lib/portal-container.js.map +1 -1
  83. package/dist/lib/seo-service.d.ts +91 -0
  84. package/dist/lib/seo-service.d.ts.map +1 -1
  85. package/dist/lib/seo-service.js +55 -2
  86. package/dist/lib/seo-service.js.map +1 -1
  87. package/dist/views/ApiKeys.js +2 -2
  88. package/dist/views/Dashboard.d.ts.map +1 -1
  89. package/dist/views/Dashboard.js +12 -14
  90. package/dist/views/Dashboard.js.map +1 -1
  91. package/dist/views/MediaBrowser.d.ts.map +1 -1
  92. package/dist/views/MediaBrowser.js +35 -6
  93. package/dist/views/MediaBrowser.js.map +1 -1
  94. package/dist/views/PageEditor.js +1 -1
  95. package/dist/views/Posts/PostTypeEditor.js +3 -3
  96. package/dist/views/SEO.d.ts.map +1 -1
  97. package/dist/views/SEO.js +4 -2
  98. package/dist/views/SEO.js.map +1 -1
  99. package/dist/views/ScriptTags.js +1 -1
  100. package/dist/views/ScriptTags.js.map +1 -1
  101. package/dist/views/Users.js +1 -1
  102. package/dist/views/page-editor/EditorTopBar.d.ts +6 -1
  103. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  104. package/dist/views/page-editor/EditorTopBar.js +5 -2
  105. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  106. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  107. package/dist/views/page-editor/PageSectionEditor.js +36 -1
  108. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  109. package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
  110. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
  111. package/dist/views/page-editor/PageSettingsModal.js +2 -13
  112. package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
  113. package/dist/views/page-editor/SectionInspector.js +2 -2
  114. package/dist/views/page-editor/SectionsPanel.js +1 -1
  115. package/dist/views/post-editor/PostFieldsModal.d.ts +1 -1
  116. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  117. package/dist/views/post-editor/PostFieldsModal.js +17 -79
  118. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  119. package/dist/views/post-editor/PostHeaderPanel.js +1 -1
  120. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  121. package/dist/views/post-editor/PostSectionEditor.js +37 -1
  122. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  123. package/dist/views/seo/ContentTab.d.ts.map +1 -1
  124. package/dist/views/seo/ContentTab.js +7 -1
  125. package/dist/views/seo/ContentTab.js.map +1 -1
  126. package/dist/views/seo/LinksTab.d.ts +4 -0
  127. package/dist/views/seo/LinksTab.d.ts.map +1 -0
  128. package/dist/views/seo/LinksTab.js +111 -0
  129. package/dist/views/seo/LinksTab.js.map +1 -0
  130. package/dist/views/seo/OverviewTab.js +1 -1
  131. package/dist/views/seo/OverviewTab.js.map +1 -1
  132. package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
  133. package/dist/views/seo/RedirectsTab.js +39 -6
  134. package/dist/views/seo/RedirectsTab.js.map +1 -1
  135. package/dist/views/seo/TechnicalTab.js +2 -2
  136. package/dist/views/settings/AISettingsTab.js +1 -1
  137. package/dist/views/settings/AiFeaturesCard.js +1 -1
  138. package/dist/views/settings/AiFeaturesCard.js.map +1 -1
  139. package/dist/views/settings/AppearanceTab.js +1 -1
  140. package/dist/views/settings/BrandVoiceCard.js +1 -1
  141. package/dist/views/settings/EmailSettingsTab.js +2 -2
  142. package/dist/views/settings/GeneralSettingsTab.js +1 -1
  143. package/dist/views/settings/SecurityStatusCard.js +1 -1
  144. package/dist/views/settings/SeoSettingsTab.js +4 -4
  145. package/dist/views/settings/UpdatesTab.js +2 -2
  146. package/dist/views/settings/components.js +3 -3
  147. package/dist/views/settings/components.js.map +1 -1
  148. package/package.json +2 -2
  149. package/src/AdminRoot.tsx +3 -2
  150. package/src/LoginRoot.tsx +2 -1
  151. package/src/__tests__/components/theme-provider-accent.test.tsx +84 -0
  152. package/src/__tests__/views/portal-container.test.tsx +14 -0
  153. package/src/__tests__/views/post-fields-modal.render.test.tsx +3 -9
  154. package/src/__tests__/views/redirects-pane.render.test.tsx +6 -0
  155. package/src/__tests__/views/seo-content-pane.render.test.tsx +16 -4
  156. package/src/components/CoauthorResultPopover.tsx +1 -1
  157. package/src/components/EditorSeoAside.tsx +68 -0
  158. package/src/components/MediaPickerModal.tsx +1 -1
  159. package/src/components/SEOConfigPanel.tsx +2 -2
  160. package/src/components/SchedulePublishDialog.tsx +1 -1
  161. package/src/components/SharePreviewLinkDialog.tsx +1 -1
  162. package/src/components/ThemeProvider.tsx +3 -1
  163. package/src/components/VersionHistory.tsx +1 -1
  164. package/src/components/seo/EditorSeoScoreBadge.tsx +26 -0
  165. package/src/components/seo/SeoAccordion.tsx +76 -0
  166. package/src/components/seo/SeoAnalysisList.tsx +113 -0
  167. package/src/components/seo/SeoEditorPane.tsx +532 -250
  168. package/src/components/seo/SeoInsightsMetrics.tsx +109 -0
  169. package/src/components/seo/SeoIssueFixPanel.tsx +16 -4
  170. package/src/components/seo/SeoLinkingSection.tsx +151 -0
  171. package/src/components/seo/primitives.tsx +7 -3
  172. package/src/components/ui/Card.tsx +5 -5
  173. package/src/components/ui/InspectorPane.tsx +3 -3
  174. package/src/components/ui/PanelSection.tsx +53 -0
  175. package/src/lib/admin-root-element.ts +15 -0
  176. package/src/lib/editor-seo-content.ts +144 -0
  177. package/src/lib/portal-container.ts +3 -2
  178. package/src/lib/seo-service.ts +150 -2
  179. package/src/views/ApiKeys.tsx +2 -2
  180. package/src/views/Dashboard.tsx +40 -53
  181. package/src/views/MediaBrowser.tsx +62 -12
  182. package/src/views/PageEditor.tsx +1 -1
  183. package/src/views/Posts/PostTypeEditor.tsx +3 -3
  184. package/src/views/SEO.tsx +6 -0
  185. package/src/views/ScriptTags.tsx +2 -2
  186. package/src/views/Users.tsx +1 -1
  187. package/src/views/page-editor/EditorTopBar.tsx +27 -0
  188. package/src/views/page-editor/PageSectionEditor.tsx +58 -3
  189. package/src/views/page-editor/PageSettingsModal.tsx +1 -45
  190. package/src/views/page-editor/SectionInspector.tsx +2 -2
  191. package/src/views/page-editor/SectionsPanel.tsx +1 -1
  192. package/src/views/post-editor/PostFieldsModal.tsx +19 -138
  193. package/src/views/post-editor/PostHeaderPanel.tsx +1 -1
  194. package/src/views/post-editor/PostSectionEditor.tsx +60 -3
  195. package/src/views/seo/ContentTab.tsx +11 -5
  196. package/src/views/seo/LinksTab.tsx +462 -0
  197. package/src/views/seo/OverviewTab.tsx +1 -1
  198. package/src/views/seo/RedirectsTab.tsx +97 -4
  199. package/src/views/seo/TechnicalTab.tsx +2 -2
  200. package/src/views/settings/AISettingsTab.tsx +3 -3
  201. package/src/views/settings/AiFeaturesCard.tsx +1 -1
  202. package/src/views/settings/AppearanceTab.tsx +1 -1
  203. package/src/views/settings/BrandVoiceCard.tsx +2 -2
  204. package/src/views/settings/EmailSettingsTab.tsx +2 -2
  205. package/src/views/settings/GeneralSettingsTab.tsx +3 -3
  206. package/src/views/settings/SecurityStatusCard.tsx +1 -1
  207. package/src/views/settings/SeoSettingsTab.tsx +6 -6
  208. package/src/views/settings/UpdatesTab.tsx +3 -3
  209. 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-3 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-3 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-3 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 })] }));
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-3", 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 ? (
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-3", 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 })] })] })] }));
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-3 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: () => {
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-3 ${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 })] })] }));
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-3 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-3", 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'
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: "pt-0 text-[var(--txt)]", children: children })] }));
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-3 text-sm ${isError
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":"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"}
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.73.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.89.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 = document.querySelector<HTMLElement>('.actuate-admin')
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 controls for excerpt and SEO fields', () => {
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.getByRole('button', { name: 'Generate SEO title from content' })).toBeTruthy()
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
- // Loaded field values populate the form.
103
- expect(await screen.findByDisplayValue('Home — Example')).toBeTruthy()
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 — Example')
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-4 py-3">
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-4 py-3">
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-4 py-3">
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-4 py-3">
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-4 py-3">
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-4 py-3">
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 document.querySelector('.actuate-admin')
124
+ return getAdminRootElement()
123
125
  }
124
126
 
125
127
  function applyColorMode(resolved: 'light' | 'dark') {