@actuate-media/cms-admin 0.40.0 → 0.41.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 (53) hide show
  1. package/dist/__tests__/views/general-settings.render.test.js +23 -2
  2. package/dist/__tests__/views/general-settings.render.test.js.map +1 -1
  3. package/dist/__tests__/views/page-section-editor.test.js +50 -0
  4. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  5. package/dist/views/page-editor/EditorCanvas.d.ts +3 -1
  6. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  7. package/dist/views/page-editor/EditorCanvas.js +8 -7
  8. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  9. package/dist/views/page-editor/EditorTopBar.d.ts +13 -1
  10. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  11. package/dist/views/page-editor/EditorTopBar.js +11 -3
  12. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  13. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  14. package/dist/views/page-editor/PageSectionEditor.js +25 -1
  15. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  16. package/dist/views/page-editor/StructureNotice.d.ts +10 -0
  17. package/dist/views/page-editor/StructureNotice.d.ts.map +1 -0
  18. package/dist/views/page-editor/StructureNotice.js +16 -0
  19. package/dist/views/page-editor/StructureNotice.js.map +1 -0
  20. package/dist/views/page-editor/sections/SectionRenderer.d.ts +2 -0
  21. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  22. package/dist/views/page-editor/sections/SectionRenderer.js +22 -0
  23. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  24. package/dist/views/page-editor/sections/index.d.ts +1 -1
  25. package/dist/views/page-editor/sections/index.d.ts.map +1 -1
  26. package/dist/views/page-editor/sections/index.js +1 -1
  27. package/dist/views/page-editor/sections/index.js.map +1 -1
  28. package/dist/views/post-editor/PostEditorCanvas.d.ts +3 -1
  29. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  30. package/dist/views/post-editor/PostEditorCanvas.js +3 -2
  31. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  32. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  33. package/dist/views/post-editor/PostSectionEditor.js +25 -1
  34. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  35. package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -1
  36. package/dist/views/settings/SeoRobotsDefaultsCard.js +2 -1
  37. package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -1
  38. package/dist/views/settings/components.d.ts.map +1 -1
  39. package/dist/views/settings/components.js +1 -1
  40. package/dist/views/settings/components.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/views/general-settings.render.test.tsx +33 -2
  43. package/src/__tests__/views/page-section-editor.test.tsx +70 -0
  44. package/src/views/page-editor/EditorCanvas.tsx +29 -22
  45. package/src/views/page-editor/EditorTopBar.tsx +58 -13
  46. package/src/views/page-editor/PageSectionEditor.tsx +42 -0
  47. package/src/views/page-editor/StructureNotice.tsx +27 -0
  48. package/src/views/page-editor/sections/SectionRenderer.tsx +24 -0
  49. package/src/views/page-editor/sections/index.ts +1 -1
  50. package/src/views/post-editor/PostEditorCanvas.tsx +5 -0
  51. package/src/views/post-editor/PostSectionEditor.tsx +42 -1
  52. package/src/views/settings/SeoRobotsDefaultsCard.tsx +9 -0
  53. package/src/views/settings/components.tsx +5 -0
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
4
4
  import { LayoutTemplate, Plus } from 'lucide-react'
5
5
  import type { PageSection } from '../../lib/page-editor-service.js'
6
6
  import { SectionRenderer } from './sections/index.js'
7
+ import { StructureNotice } from './StructureNotice.js'
7
8
  import { getViewport, type ViewportId } from './viewports.js'
8
9
 
9
10
  interface EditorCanvasProps {
@@ -12,6 +13,8 @@ interface EditorCanvasProps {
12
13
  viewport: ViewportId
13
14
  onSelect: (id: string) => void
14
15
  onAddSection: () => void
16
+ /** Section types rendered as placeholders — shows the "Structure view" notice. */
17
+ structuralTypes?: string[]
15
18
  }
16
19
 
17
20
  /**
@@ -26,6 +29,7 @@ export function EditorCanvas({
26
29
  viewport,
27
30
  onSelect,
28
31
  onAddSection,
32
+ structuralTypes = [],
29
33
  }: EditorCanvasProps) {
30
34
  const vp = getViewport(viewport)
31
35
  const containerRef = useRef<HTMLDivElement>(null)
@@ -73,31 +77,34 @@ export function EditorCanvas({
73
77
  {sections.length === 0 ? (
74
78
  <EmptyCanvas onAddSection={onAddSection} />
75
79
  ) : (
76
- <div
77
- className="mx-auto"
78
- style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
79
- >
80
+ <>
81
+ <StructureNotice structuralTypes={structuralTypes} />
80
82
  <div
81
- ref={surfaceRef}
82
- className="border-border origin-top overflow-hidden rounded-xl border bg-white shadow-sm"
83
- style={{
84
- width: vp.width,
85
- transform: `scale(${scale})`,
86
- transformOrigin: 'top left',
87
- }}
83
+ className="mx-auto"
84
+ style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
88
85
  >
89
- {sections.map((section) => (
90
- <SectionRenderer
91
- key={section.id}
92
- section={section}
93
- editable
94
- selected={section.id === selectedId}
95
- onSelect={onSelect}
96
- registerRef={registerRef}
97
- />
98
- ))}
86
+ <div
87
+ ref={surfaceRef}
88
+ className="border-border origin-top overflow-hidden rounded-xl border bg-white shadow-sm"
89
+ style={{
90
+ width: vp.width,
91
+ transform: `scale(${scale})`,
92
+ transformOrigin: 'top left',
93
+ }}
94
+ >
95
+ {sections.map((section) => (
96
+ <SectionRenderer
97
+ key={section.id}
98
+ section={section}
99
+ editable
100
+ selected={section.id === selectedId}
101
+ onSelect={onSelect}
102
+ registerRef={registerRef}
103
+ />
104
+ ))}
105
+ </div>
99
106
  </div>
100
- </div>
107
+ </>
101
108
  )}
102
109
  </div>
103
110
  )
@@ -4,6 +4,7 @@ import {
4
4
  Check,
5
5
  ExternalLink,
6
6
  Eye,
7
+ History,
7
8
  Laptop,
8
9
  Loader2,
9
10
  type LucideIcon,
@@ -38,9 +39,21 @@ interface EditorTopBarProps {
38
39
  * or installs without a public URL.
39
40
  */
40
41
  onViewOnSite?: () => void
42
+ /**
43
+ * Open the version history panel. When omitted the History button is
44
+ * hidden — e.g. for unsaved documents that have no versions yet.
45
+ */
46
+ onHistory?: () => void
41
47
  onOpenSettings: () => void
42
48
  onSaveDraft: () => void
43
49
  onPublish: () => void
50
+ /**
51
+ * Section types the canvas renders as placeholders (custom / unmanaged).
52
+ * When non-empty the editor is in "structure" mode: the eye preview is
53
+ * labeled as a structure preview and "View on site" is promoted to a
54
+ * labeled button as the primary design preview.
55
+ */
56
+ structuralTypes?: string[]
44
57
  }
45
58
 
46
59
  const VIEWPORT_ICONS: Record<string, LucideIcon> = {
@@ -71,11 +84,20 @@ export function EditorTopBar({
71
84
  onBack,
72
85
  onPreview,
73
86
  onViewOnSite,
87
+ onHistory,
74
88
  onOpenSettings,
75
89
  onSaveDraft,
76
90
  onPublish,
91
+ structuralTypes = [],
77
92
  }: EditorTopBarProps) {
78
93
  const statusInfo = STATUS_TEXT[status]
94
+ // With custom section types on the page the canvas (and the eye preview,
95
+ // which reuses the admin renderers) only shows structure — the real design
96
+ // preview is the public site, so promote "View on site" to a labeled button.
97
+ const structural = structuralTypes.length > 0
98
+ const previewLabel = structural
99
+ ? 'Preview structure (custom sections show placeholders)'
100
+ : 'Preview page'
79
101
 
80
102
  return (
81
103
  <header className="bg-card border-border flex h-14 shrink-0 items-center justify-between gap-4 border-b px-4">
@@ -143,27 +165,50 @@ export function EditorTopBar({
143
165
  <Settings2 className="h-4 w-4" aria-hidden />
144
166
  </button>
145
167
 
168
+ {onHistory && (
169
+ <button
170
+ type="button"
171
+ onClick={onHistory}
172
+ aria-label="Version history"
173
+ title="Version history"
174
+ className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
175
+ >
176
+ <History className="h-4 w-4" aria-hidden />
177
+ </button>
178
+ )}
179
+
146
180
  <button
147
181
  type="button"
148
182
  onClick={onPreview}
149
- aria-label="Preview page"
150
- title="Preview page"
183
+ aria-label={previewLabel}
184
+ title={previewLabel}
151
185
  className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
152
186
  >
153
187
  <Eye className="h-4 w-4" aria-hidden />
154
188
  </button>
155
189
 
156
- {onViewOnSite && (
157
- <button
158
- type="button"
159
- onClick={onViewOnSite}
160
- aria-label="View draft on site"
161
- title="View draft on site"
162
- className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
163
- >
164
- <ExternalLink className="h-4 w-4" aria-hidden />
165
- </button>
166
- )}
190
+ {onViewOnSite &&
191
+ (structural ? (
192
+ <button
193
+ type="button"
194
+ onClick={onViewOnSite}
195
+ title="Open the draft on your public site — the real design preview"
196
+ className="border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
197
+ >
198
+ <ExternalLink className="h-3.5 w-3.5" aria-hidden />
199
+ View on site
200
+ </button>
201
+ ) : (
202
+ <button
203
+ type="button"
204
+ onClick={onViewOnSite}
205
+ aria-label="View draft on site"
206
+ title="View draft on site"
207
+ className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
208
+ >
209
+ <ExternalLink className="h-4 w-4" aria-hidden />
210
+ </button>
211
+ ))}
167
212
 
168
213
  <button
169
214
  type="button"
@@ -5,6 +5,7 @@ import { AlertTriangle, Blocks, Loader2, Lock } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
6
  import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
7
7
  import { ErrorBoundary } from '../../components/ErrorBoundary.js'
8
+ import { VersionHistory } from '../../components/VersionHistory.js'
8
9
  import {
9
10
  addSection,
10
11
  createSection,
@@ -32,6 +33,7 @@ import {
32
33
  } from '../../lib/preview-link.js'
33
34
  import type { SectionSettings, SectionTypeId } from './section-types.js'
34
35
  import { DEFAULT_VIEWPORT, type ViewportId } from './viewports.js'
36
+ import { hasCanvasRenderer } from './sections/index.js'
35
37
  import { EditorTopBar, type SaveState } from './EditorTopBar.js'
36
38
  import { SectionsPanel } from './SectionsPanel.js'
37
39
  import { EditorCanvas } from './EditorCanvas.js'
@@ -82,6 +84,7 @@ export function PageSectionEditor({
82
84
 
83
85
  const [addOpen, setAddOpen] = useState(false)
84
86
  const [settingsOpen, setSettingsOpen] = useState(false)
87
+ const [historyOpen, setHistoryOpen] = useState(false)
85
88
  const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
86
89
  const [confirmPublish, setConfirmPublish] = useState(false)
87
90
  const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
@@ -285,6 +288,23 @@ export function PageSectionEditor({
285
288
  }
286
289
  }, [page, documentId, onNavigate])
287
290
 
291
+ // After a version restore the server has already written the restored
292
+ // data — refetch so the editor reflects it instead of the stale state.
293
+ const handleRestored = useCallback(() => {
294
+ if (!page?.id) return
295
+ fetchPageForEditor(page.id)
296
+ .then((p) => {
297
+ setPage(p)
298
+ setSelectedId(null)
299
+ setDirty(false)
300
+ setSaveState('idle')
301
+ setValidation(null)
302
+ })
303
+ .catch(() => {
304
+ toast.error('Version restored — refresh the page to see it.')
305
+ })
306
+ }, [page?.id])
307
+
288
308
  // ─── Navigation with unsaved guard ───────────────────────────────────
289
309
  const guardedNavigate = useCallback(
290
310
  (path: string) => {
@@ -398,6 +418,15 @@ export function PageSectionEditor({
398
418
  new Set(page.sections.filter((s) => s.unmanaged).map((s) => s.sectionType)),
399
419
  )
400
420
 
421
+ // Section types the canvas renders as placeholders (custom + unmanaged).
422
+ // When present the editor surfaces "structure view" messaging and promotes
423
+ // "View on site" as the primary design preview.
424
+ const structuralTypes = Array.from(
425
+ new Set(
426
+ page.sections.filter((s) => !hasCanvasRenderer(s.sectionType)).map((s) => s.sectionType),
427
+ ),
428
+ )
429
+
401
430
  return (
402
431
  <ErrorBoundary>
403
432
  <div className="bg-background flex h-full flex-col overflow-hidden">
@@ -414,9 +443,11 @@ export function PageSectionEditor({
414
443
  onBack={() => guardedNavigate('/pages')}
415
444
  onPreview={handlePreview}
416
445
  onViewOnSite={page.id ? handleViewOnSite : undefined}
446
+ onHistory={page.id ? () => setHistoryOpen(true) : undefined}
417
447
  onOpenSettings={() => setSettingsOpen(true)}
418
448
  onSaveDraft={handleSaveDraft}
419
449
  onPublish={requestPublish}
450
+ structuralTypes={structuralTypes}
420
451
  />
421
452
 
422
453
  {readOnly && (
@@ -487,6 +518,7 @@ export function PageSectionEditor({
487
518
  viewport={viewport}
488
519
  onSelect={setSelectedId}
489
520
  onAddSection={() => setAddOpen(true)}
521
+ structuralTypes={structuralTypes}
490
522
  />
491
523
  </div>
492
524
 
@@ -506,6 +538,16 @@ export function PageSectionEditor({
506
538
 
507
539
  <AddSectionModal open={addOpen} onClose={() => setAddOpen(false)} onAdd={handleAdd} />
508
540
 
541
+ {page.id && (
542
+ <VersionHistory
543
+ collectionSlug="pages"
544
+ documentId={page.id}
545
+ open={historyOpen}
546
+ onClose={() => setHistoryOpen(false)}
547
+ onRestore={handleRestored}
548
+ />
549
+ )}
550
+
509
551
  <PageSettingsModal
510
552
  open={settingsOpen}
511
553
  page={page}
@@ -0,0 +1,27 @@
1
+ 'use client'
2
+
3
+ import { Puzzle } from 'lucide-react'
4
+
5
+ /**
6
+ * Banner shown above the canvas surface when the document contains section
7
+ * types the canvas can only render as placeholders (custom / unmanaged
8
+ * types). Tells authors the canvas is a structural view — the real design
9
+ * lives on the public site behind "View on site".
10
+ */
11
+ export function StructureNotice({ structuralTypes }: { structuralTypes: string[] }) {
12
+ if (structuralTypes.length === 0) return null
13
+ const count = structuralTypes.length
14
+ return (
15
+ <div
16
+ role="note"
17
+ className="border-border bg-card text-muted-foreground mx-auto mb-4 flex w-fit max-w-full items-center gap-2 rounded-lg border px-3 py-1.5 text-xs"
18
+ >
19
+ <Puzzle className="h-3.5 w-3.5 shrink-0" aria-hidden />
20
+ <span>
21
+ <span className="text-foreground font-medium">Structure view</span> — {count} custom section{' '}
22
+ type{count > 1 ? 's' : ''} shown as placeholders. Use{' '}
23
+ <span className="text-foreground font-medium">View on site</span> for the real design.
24
+ </span>
25
+ </div>
26
+ )
27
+ }
@@ -15,6 +15,30 @@ import { AuthorBioSection } from './AuthorBioSection.js'
15
15
  import { RelatedPostsSection } from './RelatedPostsSection.js'
16
16
  import { GallerySection } from './GallerySection.js'
17
17
 
18
+ /**
19
+ * Section types with a real canvas body component below. Anything else —
20
+ * config-declared custom types (`opal-*`), unmanaged sections, and the
21
+ * `form` embed — renders as a structural placeholder, so the editors use
22
+ * this set to tell authors the canvas shows structure, not design.
23
+ */
24
+ const CANVAS_RENDERED_TYPES: ReadonlySet<string> = new Set([
25
+ 'hero',
26
+ 'text',
27
+ 'feature',
28
+ 'stats',
29
+ 'cta',
30
+ 'article-body',
31
+ 'quote',
32
+ 'author-bio',
33
+ 'related-posts',
34
+ 'gallery',
35
+ ])
36
+
37
+ /** True when the canvas renders this type for real (not as a placeholder). */
38
+ export function hasCanvasRenderer(sectionType: string): boolean {
39
+ return CANVAS_RENDERED_TYPES.has(sectionType)
40
+ }
41
+
18
42
  /** Dispatch a section's body component by type. Shared by editor + preview. */
19
43
  function SectionBody({ section, context }: { section: PageSection; context?: PostRenderContext }) {
20
44
  switch (section.sectionType) {
@@ -1,4 +1,4 @@
1
- export { SectionRenderer } from './SectionRenderer.js'
1
+ export { SectionRenderer, hasCanvasRenderer } from './SectionRenderer.js'
2
2
  export type { SectionRendererProps } from './SectionRenderer.js'
3
3
  export type { PostRenderContext } from './parts.js'
4
4
  export { HeroBannerSection } from './HeroBannerSection.js'
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
4
4
  import { FileText, Plus } from 'lucide-react'
5
5
  import type { PageSection, PostHeaderConfig } from '../../lib/post-editor-service.js'
6
6
  import { SectionRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
7
+ import { StructureNotice } from '../page-editor/StructureNotice.js'
7
8
  import { getViewport, type ViewportId } from '../page-editor/viewports.js'
8
9
  import { PostHeader } from './PostHeader.js'
9
10
 
@@ -15,6 +16,8 @@ interface PostEditorCanvasProps {
15
16
  viewport: ViewportId
16
17
  onSelect: (id: string) => void
17
18
  onAddSection: () => void
19
+ /** Section types rendered as placeholders — shows the "Structure view" notice. */
20
+ structuralTypes?: string[]
18
21
  }
19
22
 
20
23
  /**
@@ -31,6 +34,7 @@ export function PostEditorCanvas({
31
34
  viewport,
32
35
  onSelect,
33
36
  onAddSection,
37
+ structuralTypes = [],
34
38
  }: PostEditorCanvasProps) {
35
39
  const vp = getViewport(viewport)
36
40
  const containerRef = useRef<HTMLDivElement>(null)
@@ -70,6 +74,7 @@ export function PostEditorCanvas({
70
74
 
71
75
  return (
72
76
  <div ref={containerRef} className="bg-muted h-full overflow-auto p-6">
77
+ <StructureNotice structuralTypes={structuralTypes} />
73
78
  <div
74
79
  className="mx-auto"
75
80
  style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
@@ -5,6 +5,7 @@ import { AlertTriangle, Loader2 } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
6
  import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
7
7
  import { ErrorBoundary } from '../../components/ErrorBoundary.js'
8
+ import { VersionHistory } from '../../components/VersionHistory.js'
8
9
  import {
9
10
  addSection,
10
11
  createSection,
@@ -33,7 +34,7 @@ import {
33
34
  resolveSiteUrl,
34
35
  } from '../../lib/preview-link.js'
35
36
  import type { SectionSettings, SectionTypeId } from '../page-editor/section-types.js'
36
- import type { PostRenderContext } from '../page-editor/sections/index.js'
37
+ import { hasCanvasRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
37
38
  import { DEFAULT_VIEWPORT, type ViewportId } from '../page-editor/viewports.js'
38
39
  import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
39
40
  import { SectionsPanel } from '../page-editor/SectionsPanel.js'
@@ -104,6 +105,7 @@ export function PostSectionEditor({
104
105
 
105
106
  const [addOpen, setAddOpen] = useState(false)
106
107
  const [fieldsOpen, setFieldsOpen] = useState(false)
108
+ const [historyOpen, setHistoryOpen] = useState(false)
107
109
  const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
108
110
  const [confirmPublish, setConfirmPublish] = useState(false)
109
111
  const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
@@ -302,6 +304,23 @@ export function PostSectionEditor({
302
304
  }
303
305
  }, [post, documentId, postType, onNavigate])
304
306
 
307
+ // After a version restore the server has already written the restored
308
+ // data — refetch so the editor reflects it instead of the stale state.
309
+ const handleRestored = useCallback(() => {
310
+ if (!post?.id) return
311
+ fetchPostForEditor(postType, post.id)
312
+ .then((p) => {
313
+ setPost(p)
314
+ setSelectedId(null)
315
+ setDirty(false)
316
+ setSaveState('idle')
317
+ setValidation(null)
318
+ })
319
+ .catch(() => {
320
+ toast.error('Version restored — refresh the page to see it.')
321
+ })
322
+ }, [postType, post?.id])
323
+
305
324
  const guardedNavigate = useCallback(
306
325
  (path: string) => {
307
326
  if (dirty) setLeaveTarget(path)
@@ -400,6 +419,15 @@ export function PostSectionEditor({
400
419
 
401
420
  const selected = post.sections.find((s) => s.id === selectedId) ?? null
402
421
 
422
+ // Section types the canvas renders as placeholders (custom + unmanaged).
423
+ // When present the editor surfaces "structure view" messaging and promotes
424
+ // "View on site" as the primary design preview.
425
+ const structuralTypes = Array.from(
426
+ new Set(
427
+ post.sections.filter((s) => !hasCanvasRenderer(s.sectionType)).map((s) => s.sectionType),
428
+ ),
429
+ )
430
+
403
431
  return (
404
432
  <ErrorBoundary>
405
433
  <div className="bg-background flex h-full flex-col overflow-hidden">
@@ -417,9 +445,11 @@ export function PostSectionEditor({
417
445
  onBack={() => guardedNavigate('/posts')}
418
446
  onPreview={handlePreview}
419
447
  onViewOnSite={post.id ? handleViewOnSite : undefined}
448
+ onHistory={post.id ? () => setHistoryOpen(true) : undefined}
420
449
  onOpenSettings={() => setFieldsOpen(true)}
421
450
  onSaveDraft={handleSaveDraft}
422
451
  onPublish={requestPublish}
452
+ structuralTypes={structuralTypes}
423
453
  />
424
454
 
425
455
  {validation && (
@@ -452,6 +482,7 @@ export function PostSectionEditor({
452
482
  viewport={viewport}
453
483
  onSelect={setSelectedId}
454
484
  onAddSection={() => setAddOpen(true)}
485
+ structuralTypes={structuralTypes}
455
486
  />
456
487
  </div>
457
488
 
@@ -476,6 +507,16 @@ export function PostSectionEditor({
476
507
  onAdd={handleAdd}
477
508
  />
478
509
 
510
+ {post.id && (
511
+ <VersionHistory
512
+ collectionSlug={postType}
513
+ documentId={post.id}
514
+ open={historyOpen}
515
+ onClose={() => setHistoryOpen(false)}
516
+ onRestore={handleRestored}
517
+ />
518
+ )}
519
+
479
520
  <PostFieldsModal
480
521
  open={fieldsOpen}
481
522
  post={post}
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useState } from 'react'
4
+ import { Info } from 'lucide-react'
4
5
  import { ConfirmDangerousSettingDialog, SettingsCard, SettingsToggleRow } from './components.js'
5
6
  import type { GeneralSettingsForm, SiteIndexStatus } from './useGeneralSettings.js'
6
7
 
@@ -90,6 +91,14 @@ export function SeoRobotsDefaultsCard({
90
91
  disabled={!canEdit}
91
92
  onChange={(v) => requestToggle('noIndexNonProduction', v)}
92
93
  />
94
+ {isProduction && (
95
+ <p className="text-muted-foreground flex items-start gap-2 text-sm">
96
+ <Info size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
97
+ This deployment is running in the production environment, so this setting does not
98
+ change pages served here — it forces noindex only on preview, staging, and development
99
+ deployments. To block search engines from this site, use Default No Index.
100
+ </p>
101
+ )}
93
102
  </div>
94
103
 
95
104
  <div className="border-border mt-4 space-y-2 border-t pt-4">
@@ -246,6 +246,11 @@ export function SettingsSaveBar({
246
246
  <Check size={16} aria-hidden="true" /> Saved
247
247
  </span>
248
248
  )}
249
+ {dirty && !saving && (
250
+ <span className="text-muted-foreground text-sm" aria-live="polite">
251
+ Unsaved changes
252
+ </span>
253
+ )}
249
254
  <button
250
255
  type="button"
251
256
  onClick={onReset}