@actuate-media/cms-admin 0.73.0 → 0.74.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 (161) hide show
  1. package/CHANGELOG.md +16 -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/redirects-pane.render.test.js +6 -0
  15. package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
  16. package/dist/__tests__/views/seo-content-pane.render.test.js +13 -4
  17. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
  18. package/dist/actuate-admin.css +1 -1
  19. package/dist/components/CoauthorResultPopover.js +1 -1
  20. package/dist/components/EditorSeoAside.d.ts +20 -0
  21. package/dist/components/EditorSeoAside.d.ts.map +1 -0
  22. package/dist/components/EditorSeoAside.js +30 -0
  23. package/dist/components/EditorSeoAside.js.map +1 -0
  24. package/dist/components/MediaPickerModal.js +1 -1
  25. package/dist/components/SEOConfigPanel.js +1 -1
  26. package/dist/components/SchedulePublishDialog.js +1 -1
  27. package/dist/components/SharePreviewLinkDialog.js +1 -1
  28. package/dist/components/ThemeProvider.d.ts.map +1 -1
  29. package/dist/components/ThemeProvider.js +2 -1
  30. package/dist/components/ThemeProvider.js.map +1 -1
  31. package/dist/components/VersionHistory.js +1 -1
  32. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  33. package/dist/components/seo/SeoEditorPane.js +28 -8
  34. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  35. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  36. package/dist/components/seo/SeoIssueFixPanel.js +13 -3
  37. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  38. package/dist/components/seo/primitives.d.ts.map +1 -1
  39. package/dist/components/seo/primitives.js +3 -3
  40. package/dist/components/seo/primitives.js.map +1 -1
  41. package/dist/components/ui/Card.js +5 -5
  42. package/dist/components/ui/Card.js.map +1 -1
  43. package/dist/components/ui/InspectorPane.js +3 -3
  44. package/dist/components/ui/InspectorPane.js.map +1 -1
  45. package/dist/components/ui/PanelSection.d.ts +16 -0
  46. package/dist/components/ui/PanelSection.d.ts.map +1 -0
  47. package/dist/components/ui/PanelSection.js +15 -0
  48. package/dist/components/ui/PanelSection.js.map +1 -0
  49. package/dist/lib/admin-root-element.d.ts +9 -0
  50. package/dist/lib/admin-root-element.d.ts.map +1 -0
  51. package/dist/lib/admin-root-element.js +14 -0
  52. package/dist/lib/admin-root-element.js.map +1 -0
  53. package/dist/lib/portal-container.d.ts.map +1 -1
  54. package/dist/lib/portal-container.js +2 -3
  55. package/dist/lib/portal-container.js.map +1 -1
  56. package/dist/lib/seo-service.d.ts +89 -0
  57. package/dist/lib/seo-service.d.ts.map +1 -1
  58. package/dist/lib/seo-service.js +50 -2
  59. package/dist/lib/seo-service.js.map +1 -1
  60. package/dist/views/ApiKeys.js +2 -2
  61. package/dist/views/Dashboard.d.ts.map +1 -1
  62. package/dist/views/Dashboard.js +12 -14
  63. package/dist/views/Dashboard.js.map +1 -1
  64. package/dist/views/MediaBrowser.d.ts.map +1 -1
  65. package/dist/views/MediaBrowser.js +35 -6
  66. package/dist/views/MediaBrowser.js.map +1 -1
  67. package/dist/views/PageEditor.js +1 -1
  68. package/dist/views/Posts/PostTypeEditor.js +3 -3
  69. package/dist/views/SEO.d.ts.map +1 -1
  70. package/dist/views/SEO.js +4 -2
  71. package/dist/views/SEO.js.map +1 -1
  72. package/dist/views/ScriptTags.js +1 -1
  73. package/dist/views/ScriptTags.js.map +1 -1
  74. package/dist/views/Users.js +1 -1
  75. package/dist/views/page-editor/EditorTopBar.d.ts +4 -1
  76. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  77. package/dist/views/page-editor/EditorTopBar.js +5 -3
  78. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  79. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  80. package/dist/views/page-editor/PageSectionEditor.js +24 -1
  81. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  82. package/dist/views/page-editor/SectionInspector.js +2 -2
  83. package/dist/views/page-editor/SectionsPanel.js +1 -1
  84. package/dist/views/post-editor/PostHeaderPanel.js +1 -1
  85. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  86. package/dist/views/post-editor/PostSectionEditor.js +24 -1
  87. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  88. package/dist/views/seo/LinksTab.d.ts +4 -0
  89. package/dist/views/seo/LinksTab.d.ts.map +1 -0
  90. package/dist/views/seo/LinksTab.js +111 -0
  91. package/dist/views/seo/LinksTab.js.map +1 -0
  92. package/dist/views/seo/OverviewTab.js +1 -1
  93. package/dist/views/seo/OverviewTab.js.map +1 -1
  94. package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
  95. package/dist/views/seo/RedirectsTab.js +39 -6
  96. package/dist/views/seo/RedirectsTab.js.map +1 -1
  97. package/dist/views/seo/TechnicalTab.js +2 -2
  98. package/dist/views/settings/AISettingsTab.js +1 -1
  99. package/dist/views/settings/AiFeaturesCard.js +1 -1
  100. package/dist/views/settings/AiFeaturesCard.js.map +1 -1
  101. package/dist/views/settings/AppearanceTab.js +1 -1
  102. package/dist/views/settings/BrandVoiceCard.js +1 -1
  103. package/dist/views/settings/EmailSettingsTab.js +2 -2
  104. package/dist/views/settings/GeneralSettingsTab.js +1 -1
  105. package/dist/views/settings/SecurityStatusCard.js +1 -1
  106. package/dist/views/settings/SeoSettingsTab.js +4 -4
  107. package/dist/views/settings/UpdatesTab.js +2 -2
  108. package/dist/views/settings/components.js +3 -3
  109. package/dist/views/settings/components.js.map +1 -1
  110. package/package.json +2 -2
  111. package/src/AdminRoot.tsx +3 -2
  112. package/src/LoginRoot.tsx +2 -1
  113. package/src/__tests__/components/theme-provider-accent.test.tsx +84 -0
  114. package/src/__tests__/views/portal-container.test.tsx +14 -0
  115. package/src/__tests__/views/redirects-pane.render.test.tsx +6 -0
  116. package/src/__tests__/views/seo-content-pane.render.test.tsx +14 -4
  117. package/src/components/CoauthorResultPopover.tsx +1 -1
  118. package/src/components/EditorSeoAside.tsx +149 -0
  119. package/src/components/MediaPickerModal.tsx +1 -1
  120. package/src/components/SEOConfigPanel.tsx +2 -2
  121. package/src/components/SchedulePublishDialog.tsx +1 -1
  122. package/src/components/SharePreviewLinkDialog.tsx +1 -1
  123. package/src/components/ThemeProvider.tsx +3 -1
  124. package/src/components/VersionHistory.tsx +1 -1
  125. package/src/components/seo/SeoEditorPane.tsx +41 -12
  126. package/src/components/seo/SeoIssueFixPanel.tsx +16 -4
  127. package/src/components/seo/primitives.tsx +7 -3
  128. package/src/components/ui/Card.tsx +5 -5
  129. package/src/components/ui/InspectorPane.tsx +3 -3
  130. package/src/components/ui/PanelSection.tsx +53 -0
  131. package/src/lib/admin-root-element.ts +15 -0
  132. package/src/lib/portal-container.ts +3 -2
  133. package/src/lib/seo-service.ts +143 -2
  134. package/src/views/ApiKeys.tsx +2 -2
  135. package/src/views/Dashboard.tsx +40 -53
  136. package/src/views/MediaBrowser.tsx +62 -12
  137. package/src/views/PageEditor.tsx +1 -1
  138. package/src/views/Posts/PostTypeEditor.tsx +3 -3
  139. package/src/views/SEO.tsx +6 -0
  140. package/src/views/ScriptTags.tsx +2 -2
  141. package/src/views/Users.tsx +1 -1
  142. package/src/views/page-editor/EditorTopBar.tsx +24 -0
  143. package/src/views/page-editor/PageSectionEditor.tsx +43 -3
  144. package/src/views/page-editor/SectionInspector.tsx +2 -2
  145. package/src/views/page-editor/SectionsPanel.tsx +1 -1
  146. package/src/views/post-editor/PostHeaderPanel.tsx +1 -1
  147. package/src/views/post-editor/PostSectionEditor.tsx +43 -3
  148. package/src/views/seo/LinksTab.tsx +462 -0
  149. package/src/views/seo/OverviewTab.tsx +1 -1
  150. package/src/views/seo/RedirectsTab.tsx +97 -4
  151. package/src/views/seo/TechnicalTab.tsx +2 -2
  152. package/src/views/settings/AISettingsTab.tsx +3 -3
  153. package/src/views/settings/AiFeaturesCard.tsx +1 -1
  154. package/src/views/settings/AppearanceTab.tsx +1 -1
  155. package/src/views/settings/BrandVoiceCard.tsx +2 -2
  156. package/src/views/settings/EmailSettingsTab.tsx +2 -2
  157. package/src/views/settings/GeneralSettingsTab.tsx +3 -3
  158. package/src/views/settings/SecurityStatusCard.tsx +1 -1
  159. package/src/views/settings/SeoSettingsTab.tsx +6 -6
  160. package/src/views/settings/UpdatesTab.tsx +3 -3
  161. package/src/views/settings/components.tsx +3 -3
package/src/views/SEO.tsx CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  ClipboardCheck,
16
16
  RefreshCw,
17
17
  Bot,
18
+ Link2,
18
19
  } from 'lucide-react'
19
20
  import { useCallback, useEffect, useMemo, useState } from 'react'
20
21
  import { toast } from 'sonner'
@@ -30,6 +31,7 @@ import { ContentTab } from './seo/ContentTab.js'
30
31
  import { TechnicalTab } from './seo/TechnicalTab.js'
31
32
  import { RedirectsTab } from './seo/RedirectsTab.js'
32
33
  import { AuditTab } from './seo/AuditTab.js'
34
+ import { LinksTab } from './seo/LinksTab.js'
33
35
 
34
36
  export interface SEOProps {
35
37
  onNavigate?: (path: string) => void
@@ -39,6 +41,7 @@ export interface SEOProps {
39
41
  const TABS = [
40
42
  { id: 'overview', label: 'Overview', icon: LayoutDashboard },
41
43
  { id: 'content', label: 'Content SEO', icon: FileText },
44
+ { id: 'links', label: 'Links', icon: Link2 },
42
45
  { id: 'technical', label: 'Technical', icon: Settings2 },
43
46
  { id: 'redirects', label: 'Redirects', icon: ArrowRightLeft },
44
47
  { id: 'audit', label: 'Audit', icon: ClipboardCheck },
@@ -212,6 +215,9 @@ export function SEO({ onNavigate, initialTab = 'overview' }: SEOProps) {
212
215
  />
213
216
  )}
214
217
  </Tabs.Content>
218
+ <Tabs.Content value="links" tabIndex={-1}>
219
+ {activeTab === 'links' && <LinksTab onNavigate={onNavigate} />}
220
+ </Tabs.Content>
215
221
  <Tabs.Content value="technical" tabIndex={-1}>
216
222
  {activeTab === 'technical' && <TechnicalTab />}
217
223
  </Tabs.Content>
@@ -236,7 +236,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
236
236
  <div className="flex h-full min-h-0 flex-col">
237
237
  <div className="p-3 pr-6 pb-0 sm:p-4 sm:pr-8 sm:pb-0">
238
238
  {error && (
239
- <div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-3">
239
+ <div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-4">
240
240
  <AlertTriangle className="text-destructive h-5 w-5 shrink-0" />
241
241
  <span className="text-destructive flex-1 text-sm">{error}</span>
242
242
  <button
@@ -334,7 +334,7 @@ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
334
334
  </div>
335
335
 
336
336
  <SplitPaneLayout>
337
- <SplitPaneList className="p-3 pt-0 pr-6 sm:p-4 sm:pt-0 sm:pr-8">
337
+ <SplitPaneList className="p-4 pr-8 sm:pr-8">
338
338
  {tags.length === 0 && !error ? (
339
339
  <div className="rounded-lg border border-[var(--bdr)] bg-[var(--card)] p-12 text-center">
340
340
  <Code2 className="mx-auto mb-3 h-10 w-10 text-[var(--sub)]" />
@@ -982,7 +982,7 @@ export function Users({ embedded = false, currentPath }: UsersProps = {}) {
982
982
 
983
983
  {/* Role filter pills */}
984
984
  <div
985
- className="border-border flex flex-wrap gap-2 border-b px-4 py-3"
985
+ className="border-border flex flex-wrap gap-2 border-b px-6 py-4"
986
986
  role="group"
987
987
  aria-label="Filter by role"
988
988
  >
@@ -10,6 +10,7 @@ import {
10
10
  Loader2,
11
11
  type LucideIcon,
12
12
  Monitor,
13
+ Search,
13
14
  Settings2,
14
15
  Smartphone,
15
16
  Tablet,
@@ -57,6 +58,9 @@ interface EditorTopBarProps {
57
58
  */
58
59
  onToggleSitePreview?: () => void
59
60
  onOpenSettings: () => void
61
+ /** Open the document SEO panel (meta, social, schema, analysis). */
62
+ onOpenSeo?: () => void
63
+ seoOpen?: boolean
60
64
  onSaveDraft: () => void
61
65
  onPublish: () => void
62
66
  /**
@@ -102,6 +106,8 @@ export function EditorTopBar({
102
106
  sitePreview = false,
103
107
  onToggleSitePreview,
104
108
  onOpenSettings,
109
+ onOpenSeo,
110
+ seoOpen = false,
105
111
  onSaveDraft,
106
112
  onPublish,
107
113
  structuralTypes = [],
@@ -192,6 +198,24 @@ export function EditorTopBar({
192
198
  <Settings2 className="h-4 w-4" aria-hidden />
193
199
  </button>
194
200
 
201
+ {onOpenSeo && (
202
+ <button
203
+ type="button"
204
+ onClick={onOpenSeo}
205
+ aria-pressed={seoOpen}
206
+ aria-label="SEO"
207
+ title="SEO — meta tags, social preview, schema, and analysis"
208
+ className={`inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-medium transition-colors ${
209
+ seoOpen
210
+ ? 'bg-primary text-primary-foreground'
211
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent'
212
+ }`}
213
+ >
214
+ <Search className="h-4 w-4 shrink-0" aria-hidden />
215
+ <span className="hidden sm:inline">SEO</span>
216
+ </button>
217
+ )}
218
+
195
219
  {onHistory && (
196
220
  <button
197
221
  type="button"
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { AlertTriangle, Blocks, Loader2, Lock } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
6
  import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
7
+ import { EditorSeoAside } from '../../components/EditorSeoAside.js'
7
8
  import { ErrorBoundary } from '../../components/ErrorBoundary.js'
8
9
  import { VersionHistory } from '../../components/VersionHistory.js'
9
10
  import {
@@ -97,6 +98,7 @@ export function PageSectionEditor({
97
98
 
98
99
  const [addOpen, setAddOpen] = useState(false)
99
100
  const [settingsOpen, setSettingsOpen] = useState(false)
101
+ const [seoOpen, setSeoOpen] = useState(false)
100
102
  const [historyOpen, setHistoryOpen] = useState(false)
101
103
  const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
102
104
  const [confirmPublish, setConfirmPublish] = useState(false)
@@ -336,6 +338,7 @@ export function PageSectionEditor({
336
338
  .then((p) => {
337
339
  setPage(p)
338
340
  setSelectedId(null)
341
+ setSeoOpen(false)
339
342
  setDirty(false)
340
343
  setSaveState('idle')
341
344
  setValidation(null)
@@ -345,6 +348,27 @@ export function PageSectionEditor({
345
348
  })
346
349
  }, [page?.id])
347
350
 
351
+ const handleSeoSaved = useCallback(() => {
352
+ if (!page?.id) return
353
+ fetchPageForEditor(page.id)
354
+ .then((p) => setPage(p))
355
+ .catch(() => {
356
+ /* SEO API saved; editor meta will sync on next full load */
357
+ })
358
+ }, [page?.id])
359
+
360
+ const handleToggleSeo = useCallback(() => {
361
+ setSeoOpen((open) => {
362
+ if (!open) setSelectedId(null)
363
+ return !open
364
+ })
365
+ }, [])
366
+
367
+ const handleSelectSection = useCallback((id: string | null) => {
368
+ setSeoOpen(false)
369
+ setSelectedId(id)
370
+ }, [])
371
+
348
372
  // ─── Navigation with unsaved guard ───────────────────────────────────
349
373
  const guardedNavigate = useCallback(
350
374
  (path: string) => {
@@ -492,6 +516,8 @@ export function PageSectionEditor({
492
516
  onToggleSitePreview={page.id && page.slug ? sitePreview.toggle : undefined}
493
517
  onHistory={page.id ? () => setHistoryOpen(true) : undefined}
494
518
  onOpenSettings={() => setSettingsOpen(true)}
519
+ onOpenSeo={handleToggleSeo}
520
+ seoOpen={seoOpen}
495
521
  onSaveDraft={handleSaveDraft}
496
522
  onPublish={requestPublish}
497
523
  structuralTypes={structuralTypes}
@@ -550,7 +576,7 @@ export function PageSectionEditor({
550
576
  sections={page.sections}
551
577
  selectedId={selectedId}
552
578
  canEdit={effectiveCanEdit}
553
- onSelect={setSelectedId}
579
+ onSelect={handleSelectSection}
554
580
  onReorder={handleReorder}
555
581
  onToggleVisibility={handleToggleVisibility}
556
582
  onDuplicate={handleDuplicate}
@@ -571,7 +597,7 @@ export function PageSectionEditor({
571
597
  sections={page.sections}
572
598
  selectedId={selectedId}
573
599
  viewport={viewport}
574
- onSelect={setSelectedId}
600
+ onSelect={handleSelectSection}
575
601
  onAddSection={() => setAddOpen(true)}
576
602
  structuralTypes={structuralTypes}
577
603
  sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
@@ -579,7 +605,7 @@ export function PageSectionEditor({
579
605
  )}
580
606
  </div>
581
607
 
582
- {selected && (
608
+ {selected && !seoOpen && (
583
609
  <SectionInspector
584
610
  section={selected}
585
611
  canEdit={effectiveCanEdit}
@@ -590,6 +616,20 @@ export function PageSectionEditor({
590
616
  onToggleVisibility={() => handleToggleVisibility(selected.id)}
591
617
  />
592
618
  )}
619
+
620
+ {seoOpen && (
621
+ <EditorSeoAside
622
+ entityType="page"
623
+ entityId={page.id}
624
+ title={page.title}
625
+ seoTitle={page.seoTitle}
626
+ seoDescription={page.seoDescription}
627
+ canEdit={effectiveCanEdit}
628
+ onClose={() => setSeoOpen(false)}
629
+ onDraftChange={handlePageSettings}
630
+ onSaved={handleSeoSaved}
631
+ />
632
+ )}
593
633
  </div>
594
634
  </div>
595
635
 
@@ -109,7 +109,7 @@ export function SectionInspector({
109
109
  aria-label={`${section.name} (read-only)`}
110
110
  className={INSPECTOR_SHELL}
111
111
  >
112
- <div className="border-border flex items-center justify-between border-b px-4 py-3">
112
+ <div className="border-border flex items-center justify-between border-b px-6 py-4">
113
113
  <div className="min-w-0">
114
114
  <h2 className="text-foreground truncate text-sm font-medium">{section.name}</h2>
115
115
  <p className="text-muted-foreground text-xs">Managed outside the editor</p>
@@ -142,7 +142,7 @@ export function SectionInspector({
142
142
 
143
143
  return (
144
144
  <ResizableAside role="dialog" aria-label={`Edit ${section.name}`} className={INSPECTOR_SHELL}>
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="min-w-0">
147
147
  <h2 className="text-foreground truncate text-sm font-medium">{section.name}</h2>
148
148
  <p className="text-muted-foreground text-xs">{def?.name ?? 'Section'}</p>
@@ -78,7 +78,7 @@ export function SectionsPanel({
78
78
  className="bg-card border-border flex h-full w-72 shrink-0 flex-col border-r"
79
79
  aria-label="Page sections"
80
80
  >
81
- <div className="border-border flex items-center justify-between border-b px-4 py-3">
81
+ <div className="border-border flex items-center justify-between border-b px-6 py-4">
82
82
  <div className="flex items-center gap-2">
83
83
  <h2 className="text-foreground text-sm font-medium">Sections</h2>
84
84
  <span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium tabular-nums">
@@ -33,7 +33,7 @@ export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHead
33
33
  aria-label="Edit post header"
34
34
  className="bg-card border-border flex h-full flex-col border-l"
35
35
  >
36
- <div className="border-border flex items-center justify-between border-b px-4 py-3">
36
+ <div className="border-border flex items-center justify-between border-b px-6 py-4">
37
37
  <div className="min-w-0">
38
38
  <h2 className="text-foreground truncate text-sm font-medium">Post header</h2>
39
39
  <p className="text-muted-foreground text-xs">Layout for every post of this type</p>
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { AlertTriangle, Loader2, Lock } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
6
  import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
7
+ import { EditorSeoAside } from '../../components/EditorSeoAside.js'
7
8
  import { ErrorBoundary } from '../../components/ErrorBoundary.js'
8
9
  import { VersionHistory } from '../../components/VersionHistory.js'
9
10
  import {
@@ -122,6 +123,7 @@ export function PostSectionEditor({
122
123
 
123
124
  const [addOpen, setAddOpen] = useState(false)
124
125
  const [fieldsOpen, setFieldsOpen] = useState(false)
126
+ const [seoOpen, setSeoOpen] = useState(false)
125
127
  const [historyOpen, setHistoryOpen] = useState(false)
126
128
  const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
127
129
  const [confirmPublish, setConfirmPublish] = useState(false)
@@ -386,6 +388,7 @@ export function PostSectionEditor({
386
388
  .then((p) => {
387
389
  setPost(p)
388
390
  setSelectedId(null)
391
+ setSeoOpen(false)
389
392
  setDirty(false)
390
393
  setSaveState('idle')
391
394
  setValidation(null)
@@ -395,6 +398,27 @@ export function PostSectionEditor({
395
398
  })
396
399
  }, [postType, post?.id])
397
400
 
401
+ const handleSeoSaved = useCallback(() => {
402
+ if (!post?.id) return
403
+ fetchPostForEditor(postType, post.id)
404
+ .then((p) => setPost(p))
405
+ .catch(() => {
406
+ /* SEO API saved; editor meta will sync on next full load */
407
+ })
408
+ }, [postType, post?.id])
409
+
410
+ const handleToggleSeo = useCallback(() => {
411
+ setSeoOpen((open) => {
412
+ if (!open) setSelectedId(null)
413
+ return !open
414
+ })
415
+ }, [])
416
+
417
+ const handleSelectSection = useCallback((id: string | null) => {
418
+ setSeoOpen(false)
419
+ setSelectedId(id)
420
+ }, [])
421
+
398
422
  const guardedNavigate = useCallback(
399
423
  (path: string) => {
400
424
  if (dirty) setLeaveTarget(path)
@@ -534,6 +558,8 @@ export function PostSectionEditor({
534
558
  onToggleSitePreview={post.id && post.slug ? sitePreview.toggle : undefined}
535
559
  onHistory={post.id ? () => setHistoryOpen(true) : undefined}
536
560
  onOpenSettings={() => setFieldsOpen(true)}
561
+ onOpenSeo={handleToggleSeo}
562
+ seoOpen={seoOpen}
537
563
  onSaveDraft={handleSaveDraft}
538
564
  onPublish={requestPublish}
539
565
  structuralTypes={structuralTypes}
@@ -569,7 +595,7 @@ export function PostSectionEditor({
569
595
  sections={post.sections}
570
596
  selectedId={selectedId}
571
597
  canEdit={canEdit}
572
- onSelect={setSelectedId}
598
+ onSelect={handleSelectSection}
573
599
  onReorder={handleReorder}
574
600
  onToggleVisibility={handleToggleVisibility}
575
601
  onDuplicate={handleDuplicate}
@@ -592,7 +618,7 @@ export function PostSectionEditor({
592
618
  context={context}
593
619
  selectedId={selectedId}
594
620
  viewport={viewport}
595
- onSelect={setSelectedId}
621
+ onSelect={handleSelectSection}
596
622
  onAddSection={() => setAddOpen(true)}
597
623
  structuralTypes={structuralTypes}
598
624
  sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
@@ -600,7 +626,7 @@ export function PostSectionEditor({
600
626
  )}
601
627
  </div>
602
628
 
603
- {selected && (
629
+ {selected && !seoOpen && (
604
630
  <SectionInspector
605
631
  section={selected}
606
632
  canEdit={canEdit}
@@ -613,6 +639,20 @@ export function PostSectionEditor({
613
639
  onPostBodyChange={handlePostBodyChange}
614
640
  />
615
641
  )}
642
+
643
+ {seoOpen && (
644
+ <EditorSeoAside
645
+ entityType="post"
646
+ entityId={post.id}
647
+ title={post.title}
648
+ seoTitle={post.seoTitle}
649
+ seoDescription={post.seoDescription}
650
+ canEdit={canEdit}
651
+ onClose={() => setSeoOpen(false)}
652
+ onDraftChange={handleFields}
653
+ onSaved={handleSeoSaved}
654
+ />
655
+ )}
616
656
  </div>
617
657
  </div>
618
658