@actuate-media/cms-admin 0.72.3 → 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 (178) hide show
  1. package/CHANGELOG.md +22 -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/resizable-aside.test.d.ts +2 -0
  9. package/dist/__tests__/components/resizable-aside.test.d.ts.map +1 -0
  10. package/dist/__tests__/components/resizable-aside.test.js +77 -0
  11. package/dist/__tests__/components/resizable-aside.test.js.map +1 -0
  12. package/dist/__tests__/components/theme-provider-accent.test.d.ts +2 -0
  13. package/dist/__tests__/components/theme-provider-accent.test.d.ts.map +1 -0
  14. package/dist/__tests__/components/theme-provider-accent.test.js +64 -0
  15. package/dist/__tests__/components/theme-provider-accent.test.js.map +1 -0
  16. package/dist/__tests__/views/portal-container.test.js +13 -0
  17. package/dist/__tests__/views/portal-container.test.js.map +1 -1
  18. package/dist/__tests__/views/redirects-pane.render.test.js +6 -0
  19. package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
  20. package/dist/__tests__/views/seo-content-pane.render.test.js +13 -4
  21. package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
  22. package/dist/actuate-admin.css +1 -1
  23. package/dist/components/CoauthorResultPopover.js +1 -1
  24. package/dist/components/EditorSeoAside.d.ts +20 -0
  25. package/dist/components/EditorSeoAside.d.ts.map +1 -0
  26. package/dist/components/EditorSeoAside.js +30 -0
  27. package/dist/components/EditorSeoAside.js.map +1 -0
  28. package/dist/components/MediaPickerModal.js +1 -1
  29. package/dist/components/SEOConfigPanel.js +1 -1
  30. package/dist/components/SchedulePublishDialog.js +1 -1
  31. package/dist/components/SharePreviewLinkDialog.js +1 -1
  32. package/dist/components/ThemeProvider.d.ts.map +1 -1
  33. package/dist/components/ThemeProvider.js +2 -1
  34. package/dist/components/ThemeProvider.js.map +1 -1
  35. package/dist/components/VersionHistory.js +1 -1
  36. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  37. package/dist/components/seo/SeoEditorPane.js +28 -8
  38. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  39. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  40. package/dist/components/seo/SeoIssueFixPanel.js +13 -3
  41. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  42. package/dist/components/seo/primitives.d.ts.map +1 -1
  43. package/dist/components/seo/primitives.js +3 -3
  44. package/dist/components/seo/primitives.js.map +1 -1
  45. package/dist/components/ui/Card.js +5 -5
  46. package/dist/components/ui/Card.js.map +1 -1
  47. package/dist/components/ui/InspectorPane.js +3 -3
  48. package/dist/components/ui/InspectorPane.js.map +1 -1
  49. package/dist/components/ui/PanelSection.d.ts +16 -0
  50. package/dist/components/ui/PanelSection.d.ts.map +1 -0
  51. package/dist/components/ui/PanelSection.js +15 -0
  52. package/dist/components/ui/PanelSection.js.map +1 -0
  53. package/dist/components/ui/ResizableAside.d.ts +38 -0
  54. package/dist/components/ui/ResizableAside.d.ts.map +1 -0
  55. package/dist/components/ui/ResizableAside.js +209 -0
  56. package/dist/components/ui/ResizableAside.js.map +1 -0
  57. package/dist/lib/admin-root-element.d.ts +9 -0
  58. package/dist/lib/admin-root-element.d.ts.map +1 -0
  59. package/dist/lib/admin-root-element.js +14 -0
  60. package/dist/lib/admin-root-element.js.map +1 -0
  61. package/dist/lib/portal-container.d.ts.map +1 -1
  62. package/dist/lib/portal-container.js +2 -3
  63. package/dist/lib/portal-container.js.map +1 -1
  64. package/dist/lib/seo-service.d.ts +89 -0
  65. package/dist/lib/seo-service.d.ts.map +1 -1
  66. package/dist/lib/seo-service.js +50 -2
  67. package/dist/lib/seo-service.js.map +1 -1
  68. package/dist/views/ApiKeys.js +2 -2
  69. package/dist/views/Dashboard.d.ts.map +1 -1
  70. package/dist/views/Dashboard.js +12 -14
  71. package/dist/views/Dashboard.js.map +1 -1
  72. package/dist/views/MediaBrowser.d.ts.map +1 -1
  73. package/dist/views/MediaBrowser.js +35 -6
  74. package/dist/views/MediaBrowser.js.map +1 -1
  75. package/dist/views/PageEditor.js +1 -1
  76. package/dist/views/Posts/PostTypeEditor.js +3 -3
  77. package/dist/views/SEO.d.ts.map +1 -1
  78. package/dist/views/SEO.js +4 -2
  79. package/dist/views/SEO.js.map +1 -1
  80. package/dist/views/ScriptTags.js +1 -1
  81. package/dist/views/ScriptTags.js.map +1 -1
  82. package/dist/views/Users.js +1 -1
  83. package/dist/views/page-editor/EditorTopBar.d.ts +4 -1
  84. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  85. package/dist/views/page-editor/EditorTopBar.js +5 -3
  86. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  87. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  88. package/dist/views/page-editor/PageSectionEditor.js +24 -1
  89. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  90. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  91. package/dist/views/page-editor/SectionInspector.js +7 -2
  92. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  93. package/dist/views/page-editor/SectionsPanel.js +1 -1
  94. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -1
  95. package/dist/views/post-editor/PostHeaderPanel.js +2 -1
  96. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
  97. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  98. package/dist/views/post-editor/PostSectionEditor.js +24 -1
  99. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  100. package/dist/views/post-editor/PostTemplateEditor.js +1 -1
  101. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
  102. package/dist/views/seo/LinksTab.d.ts +4 -0
  103. package/dist/views/seo/LinksTab.d.ts.map +1 -0
  104. package/dist/views/seo/LinksTab.js +111 -0
  105. package/dist/views/seo/LinksTab.js.map +1 -0
  106. package/dist/views/seo/OverviewTab.js +1 -1
  107. package/dist/views/seo/OverviewTab.js.map +1 -1
  108. package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
  109. package/dist/views/seo/RedirectsTab.js +39 -6
  110. package/dist/views/seo/RedirectsTab.js.map +1 -1
  111. package/dist/views/seo/TechnicalTab.js +2 -2
  112. package/dist/views/settings/AISettingsTab.js +1 -1
  113. package/dist/views/settings/AiFeaturesCard.js +1 -1
  114. package/dist/views/settings/AiFeaturesCard.js.map +1 -1
  115. package/dist/views/settings/AppearanceTab.js +1 -1
  116. package/dist/views/settings/BrandVoiceCard.js +1 -1
  117. package/dist/views/settings/EmailSettingsTab.js +2 -2
  118. package/dist/views/settings/GeneralSettingsTab.js +1 -1
  119. package/dist/views/settings/SecurityStatusCard.js +1 -1
  120. package/dist/views/settings/SeoSettingsTab.js +4 -4
  121. package/dist/views/settings/UpdatesTab.js +2 -2
  122. package/dist/views/settings/components.js +3 -3
  123. package/dist/views/settings/components.js.map +1 -1
  124. package/package.json +2 -2
  125. package/src/AdminRoot.tsx +3 -2
  126. package/src/LoginRoot.tsx +2 -1
  127. package/src/__tests__/components/resizable-aside.test.tsx +130 -0
  128. package/src/__tests__/components/theme-provider-accent.test.tsx +84 -0
  129. package/src/__tests__/views/portal-container.test.tsx +14 -0
  130. package/src/__tests__/views/redirects-pane.render.test.tsx +6 -0
  131. package/src/__tests__/views/seo-content-pane.render.test.tsx +14 -4
  132. package/src/components/CoauthorResultPopover.tsx +1 -1
  133. package/src/components/EditorSeoAside.tsx +149 -0
  134. package/src/components/MediaPickerModal.tsx +1 -1
  135. package/src/components/SEOConfigPanel.tsx +2 -2
  136. package/src/components/SchedulePublishDialog.tsx +1 -1
  137. package/src/components/SharePreviewLinkDialog.tsx +1 -1
  138. package/src/components/ThemeProvider.tsx +3 -1
  139. package/src/components/VersionHistory.tsx +1 -1
  140. package/src/components/seo/SeoEditorPane.tsx +41 -12
  141. package/src/components/seo/SeoIssueFixPanel.tsx +16 -4
  142. package/src/components/seo/primitives.tsx +7 -3
  143. package/src/components/ui/Card.tsx +5 -5
  144. package/src/components/ui/InspectorPane.tsx +3 -3
  145. package/src/components/ui/PanelSection.tsx +53 -0
  146. package/src/components/ui/ResizableAside.tsx +317 -0
  147. package/src/lib/admin-root-element.ts +15 -0
  148. package/src/lib/portal-container.ts +3 -2
  149. package/src/lib/seo-service.ts +143 -2
  150. package/src/views/ApiKeys.tsx +2 -2
  151. package/src/views/Dashboard.tsx +40 -53
  152. package/src/views/MediaBrowser.tsx +62 -12
  153. package/src/views/PageEditor.tsx +1 -1
  154. package/src/views/Posts/PostTypeEditor.tsx +3 -3
  155. package/src/views/SEO.tsx +6 -0
  156. package/src/views/ScriptTags.tsx +2 -2
  157. package/src/views/Users.tsx +1 -1
  158. package/src/views/page-editor/EditorTopBar.tsx +24 -0
  159. package/src/views/page-editor/PageSectionEditor.tsx +44 -4
  160. package/src/views/page-editor/SectionInspector.tsx +33 -27
  161. package/src/views/page-editor/SectionsPanel.tsx +1 -1
  162. package/src/views/post-editor/PostHeaderPanel.tsx +16 -12
  163. package/src/views/post-editor/PostSectionEditor.tsx +44 -4
  164. package/src/views/post-editor/PostTemplateEditor.tsx +1 -1
  165. package/src/views/seo/LinksTab.tsx +462 -0
  166. package/src/views/seo/OverviewTab.tsx +1 -1
  167. package/src/views/seo/RedirectsTab.tsx +97 -4
  168. package/src/views/seo/TechnicalTab.tsx +2 -2
  169. package/src/views/settings/AISettingsTab.tsx +3 -3
  170. package/src/views/settings/AiFeaturesCard.tsx +1 -1
  171. package/src/views/settings/AppearanceTab.tsx +1 -1
  172. package/src/views/settings/BrandVoiceCard.tsx +2 -2
  173. package/src/views/settings/EmailSettingsTab.tsx +2 -2
  174. package/src/views/settings/GeneralSettingsTab.tsx +3 -3
  175. package/src/views/settings/SecurityStatusCard.tsx +1 -1
  176. package/src/views/settings/SeoSettingsTab.tsx +6 -6
  177. package/src/views/settings/UpdatesTab.tsx +3 -3
  178. package/src/views/settings/components.tsx +3 -3
@@ -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}
@@ -545,12 +571,12 @@ export function PageSectionEditor({
545
571
  />
546
572
  )}
547
573
 
548
- <div className="flex flex-1 overflow-hidden">
574
+ <div className="relative flex flex-1 overflow-hidden">
549
575
  <SectionsPanel
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
 
@@ -19,6 +19,7 @@ import {
19
19
  } from '@dnd-kit/sortable'
20
20
  import { CSS } from '@dnd-kit/utilities'
21
21
  import { MediaPickerModal } from '../../components/MediaPickerModal.js'
22
+ import { InspectorExpandButton, ResizableAside } from '../../components/ui/ResizableAside.js'
22
23
  import { Toggle } from '../../components/ui/Toggle.js'
23
24
  import { RichTextField } from '../../fields/RichTextField.js'
24
25
  import type { PageSection } from '../../lib/page-editor-service.js'
@@ -46,6 +47,29 @@ interface SectionInspectorProps {
46
47
  const LABEL = 'text-foreground mb-1 block text-xs font-medium'
47
48
  const INPUT =
48
49
  'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
50
+ const INSPECTOR_SHELL = 'bg-card border-border flex h-full flex-col border-l'
51
+
52
+ function InspectorHeaderActions({
53
+ onClose,
54
+ closeLabel = 'Close inspector',
55
+ }: {
56
+ onClose: () => void
57
+ closeLabel?: string
58
+ }) {
59
+ return (
60
+ <div className="flex shrink-0 items-center gap-0.5">
61
+ <InspectorExpandButton />
62
+ <button
63
+ type="button"
64
+ onClick={onClose}
65
+ aria-label={closeLabel}
66
+ className="text-muted-foreground hover:text-foreground hover:bg-accent flex h-9 w-9 items-center justify-center rounded-md md:h-7 md:w-7"
67
+ >
68
+ <X className="h-4 w-4" aria-hidden />
69
+ </button>
70
+ </div>
71
+ )
72
+ }
49
73
 
50
74
  export function SectionInspector({
51
75
  section,
@@ -80,24 +104,17 @@ export function SectionInspector({
80
104
  // editable controls, so the externally-managed content is never altered here.
81
105
  if (section.unmanaged) {
82
106
  return (
83
- <aside
107
+ <ResizableAside
84
108
  role="dialog"
85
109
  aria-label={`${section.name} (read-only)`}
86
- className="bg-card border-border flex h-full w-80 shrink-0 flex-col border-l"
110
+ className={INSPECTOR_SHELL}
87
111
  >
88
- <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">
89
113
  <div className="min-w-0">
90
114
  <h2 className="text-foreground truncate text-sm font-medium">{section.name}</h2>
91
115
  <p className="text-muted-foreground text-xs">Managed outside the editor</p>
92
116
  </div>
93
- <button
94
- type="button"
95
- onClick={onClose}
96
- aria-label="Close inspector"
97
- className="text-muted-foreground hover:text-foreground hover:bg-accent rounded p-1"
98
- >
99
- <X className="h-4 w-4" aria-hidden />
100
- </button>
117
+ <InspectorHeaderActions onClose={onClose} />
101
118
  </div>
102
119
  <div className="flex-1 space-y-3 overflow-y-auto p-4">
103
120
  <div className="border-border bg-muted text-muted-foreground space-y-2 rounded-md border p-3 text-sm">
@@ -119,29 +136,18 @@ export function SectionInspector({
119
136
  </p>
120
137
  </div>
121
138
  </div>
122
- </aside>
139
+ </ResizableAside>
123
140
  )
124
141
  }
125
142
 
126
143
  return (
127
- <aside
128
- role="dialog"
129
- aria-label={`Edit ${section.name}`}
130
- className="bg-card border-border flex h-full w-80 shrink-0 flex-col border-l"
131
- >
132
- <div className="border-border flex items-center justify-between border-b px-4 py-3">
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-6 py-4">
133
146
  <div className="min-w-0">
134
147
  <h2 className="text-foreground truncate text-sm font-medium">{section.name}</h2>
135
148
  <p className="text-muted-foreground text-xs">{def?.name ?? 'Section'}</p>
136
149
  </div>
137
- <button
138
- type="button"
139
- onClick={onClose}
140
- aria-label="Close inspector"
141
- className="text-muted-foreground hover:text-foreground hover:bg-accent rounded p-1"
142
- >
143
- <X className="h-4 w-4" aria-hidden />
144
- </button>
150
+ <InspectorHeaderActions onClose={onClose} />
145
151
  </div>
146
152
 
147
153
  <div className="flex-1 space-y-6 overflow-y-auto p-4">
@@ -316,7 +322,7 @@ export function SectionInspector({
316
322
  </div>
317
323
  </fieldset>
318
324
  </div>
319
- </aside>
325
+ </ResizableAside>
320
326
  )
321
327
  }
322
328
 
@@ -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">
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { X } from 'lucide-react'
4
+ import { InspectorExpandButton, ResizableAside } from '../../components/ui/ResizableAside.js'
4
5
  import { Toggle } from '../../components/ui/Toggle.js'
5
6
  import type { PostHeaderConfig } from '../../lib/post-editor-service.js'
6
7
 
@@ -27,24 +28,27 @@ const TOGGLES: Array<{ key: keyof PostHeaderConfig; label: string }> = [
27
28
  /** Inspector panel for the post header layout (template-level design config). */
28
29
  export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHeaderPanelProps) {
29
30
  return (
30
- <aside
31
+ <ResizableAside
31
32
  role="dialog"
32
33
  aria-label="Edit post header"
33
- className="bg-card border-border flex h-full w-80 shrink-0 flex-col border-l"
34
+ className="bg-card border-border flex h-full flex-col border-l"
34
35
  >
35
- <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">
36
37
  <div className="min-w-0">
37
38
  <h2 className="text-foreground truncate text-sm font-medium">Post header</h2>
38
39
  <p className="text-muted-foreground text-xs">Layout for every post of this type</p>
39
40
  </div>
40
- <button
41
- type="button"
42
- onClick={onClose}
43
- aria-label="Close header panel"
44
- className="text-muted-foreground hover:text-foreground hover:bg-accent rounded p-1"
45
- >
46
- <X className="h-4 w-4" aria-hidden />
47
- </button>
41
+ <div className="flex shrink-0 items-center gap-0.5">
42
+ <InspectorExpandButton />
43
+ <button
44
+ type="button"
45
+ onClick={onClose}
46
+ aria-label="Close header panel"
47
+ className="text-muted-foreground hover:text-foreground hover:bg-accent flex h-9 w-9 items-center justify-center rounded-md md:h-7 md:w-7"
48
+ >
49
+ <X className="h-4 w-4" aria-hidden />
50
+ </button>
51
+ </div>
48
52
  </div>
49
53
 
50
54
  <div className="flex-1 space-y-6 overflow-y-auto p-4">
@@ -92,6 +96,6 @@ export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHead
92
96
  })}
93
97
  </fieldset>
94
98
  </div>
95
- </aside>
99
+ </ResizableAside>
96
100
  )
97
101
  }
@@ -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}
@@ -564,12 +590,12 @@ export function PostSectionEditor({
564
590
  />
565
591
  )}
566
592
 
567
- <div className="flex flex-1 overflow-hidden">
593
+ <div className="relative flex flex-1 overflow-hidden">
568
594
  <SectionsPanel
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
 
@@ -248,7 +248,7 @@ export function PostTemplateEditor({
248
248
  </div>
249
249
  </header>
250
250
 
251
- <div className="flex flex-1 overflow-hidden">
251
+ <div className="relative flex flex-1 overflow-hidden">
252
252
  <SectionsPanel
253
253
  sections={template.sections}
254
254
  selectedId={selectedSectionId}