@prenta/admin 0.92.0 → 0.94.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 (425) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/design-system/tokens.css +173 -71
  3. package/design-system/tokens.json +244 -63
  4. package/design-system/tokens.scss +203 -98
  5. package/design-system/tokens.tailwind.cjs +93 -38
  6. package/dist/AdminRoot.d.ts +27 -6
  7. package/dist/AdminRoot.d.ts.map +1 -1
  8. package/dist/AdminRoot.js.map +1 -1
  9. package/dist/__tests__/components/breakpoint-controls.render.test.d.ts +2 -0
  10. package/dist/__tests__/components/breakpoint-controls.render.test.d.ts.map +1 -0
  11. package/dist/__tests__/components/breakpoint-controls.render.test.js +51 -0
  12. package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -0
  13. package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts +2 -0
  14. package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts.map +1 -0
  15. package/dist/__tests__/components/canvas-edit-tip.render.test.js +37 -0
  16. package/dist/__tests__/components/canvas-edit-tip.render.test.js.map +1 -0
  17. package/dist/__tests__/components/editor-rail.render.test.d.ts +2 -0
  18. package/dist/__tests__/components/editor-rail.render.test.d.ts.map +1 -0
  19. package/dist/__tests__/components/editor-rail.render.test.js +138 -0
  20. package/dist/__tests__/components/editor-rail.render.test.js.map +1 -0
  21. package/dist/__tests__/components/media-block.render.test.d.ts +2 -0
  22. package/dist/__tests__/components/media-block.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/components/media-block.render.test.js +108 -0
  24. package/dist/__tests__/components/media-block.render.test.js.map +1 -0
  25. package/dist/__tests__/components/page-audit-panel.render.test.d.ts +2 -0
  26. package/dist/__tests__/components/page-audit-panel.render.test.d.ts.map +1 -0
  27. package/dist/__tests__/components/page-audit-panel.render.test.js +59 -0
  28. package/dist/__tests__/components/page-audit-panel.render.test.js.map +1 -0
  29. package/dist/__tests__/components/reflow-meter.render.test.d.ts +2 -0
  30. package/dist/__tests__/components/reflow-meter.render.test.d.ts.map +1 -0
  31. package/dist/__tests__/components/reflow-meter.render.test.js +94 -0
  32. package/dist/__tests__/components/reflow-meter.render.test.js.map +1 -0
  33. package/dist/__tests__/components/seo-char-count.test.js +24 -6
  34. package/dist/__tests__/components/seo-char-count.test.js.map +1 -1
  35. package/dist/__tests__/components/serp-preview.render.test.d.ts +2 -0
  36. package/dist/__tests__/components/serp-preview.render.test.d.ts.map +1 -0
  37. package/dist/__tests__/components/serp-preview.render.test.js +25 -0
  38. package/dist/__tests__/components/serp-preview.render.test.js.map +1 -0
  39. package/dist/__tests__/lib/breakpoints.test.d.ts +2 -0
  40. package/dist/__tests__/lib/breakpoints.test.d.ts.map +1 -0
  41. package/dist/__tests__/lib/breakpoints.test.js +67 -0
  42. package/dist/__tests__/lib/breakpoints.test.js.map +1 -0
  43. package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts +2 -0
  44. package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts.map +1 -0
  45. package/dist/__tests__/lib/canvas-edit-chrome.test.js +44 -0
  46. package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -0
  47. package/dist/__tests__/lib/media-format.test.d.ts +2 -0
  48. package/dist/__tests__/lib/media-format.test.d.ts.map +1 -0
  49. package/dist/__tests__/lib/media-format.test.js +70 -0
  50. package/dist/__tests__/lib/media-format.test.js.map +1 -0
  51. package/dist/__tests__/lib/page-audit-input.test.d.ts +2 -0
  52. package/dist/__tests__/lib/page-audit-input.test.d.ts.map +1 -0
  53. package/dist/__tests__/lib/page-audit-input.test.js +122 -0
  54. package/dist/__tests__/lib/page-audit-input.test.js.map +1 -0
  55. package/dist/__tests__/lib/page-editor-service.test.js +2 -0
  56. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
  57. package/dist/__tests__/lib/page-redirect.test.d.ts +2 -0
  58. package/dist/__tests__/lib/page-redirect.test.d.ts.map +1 -0
  59. package/dist/__tests__/lib/page-redirect.test.js +26 -0
  60. package/dist/__tests__/lib/page-redirect.test.js.map +1 -0
  61. package/dist/__tests__/lib/rail-width.test.d.ts +2 -0
  62. package/dist/__tests__/lib/rail-width.test.d.ts.map +1 -0
  63. package/dist/__tests__/lib/rail-width.test.js +69 -0
  64. package/dist/__tests__/lib/rail-width.test.js.map +1 -0
  65. package/dist/__tests__/lib/reflow-baseline.test.d.ts +2 -0
  66. package/dist/__tests__/lib/reflow-baseline.test.d.ts.map +1 -0
  67. package/dist/__tests__/lib/reflow-baseline.test.js +56 -0
  68. package/dist/__tests__/lib/reflow-baseline.test.js.map +1 -0
  69. package/dist/__tests__/lib/reflow-probe.test.d.ts +2 -0
  70. package/dist/__tests__/lib/reflow-probe.test.d.ts.map +1 -0
  71. package/dist/__tests__/lib/reflow-probe.test.js +73 -0
  72. package/dist/__tests__/lib/reflow-probe.test.js.map +1 -0
  73. package/dist/__tests__/lib/version-diff.test.d.ts +2 -0
  74. package/dist/__tests__/lib/version-diff.test.d.ts.map +1 -0
  75. package/dist/__tests__/lib/version-diff.test.js +230 -0
  76. package/dist/__tests__/lib/version-diff.test.js.map +1 -0
  77. package/dist/__tests__/lib/version-history.test.d.ts +2 -0
  78. package/dist/__tests__/lib/version-history.test.d.ts.map +1 -0
  79. package/dist/__tests__/lib/version-history.test.js +125 -0
  80. package/dist/__tests__/lib/version-history.test.js.map +1 -0
  81. package/dist/__tests__/styles/canvas-container-queries.test.d.ts +2 -0
  82. package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
  83. package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
  84. package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
  85. package/dist/__tests__/styles/packaging.test.d.ts +2 -0
  86. package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
  87. package/dist/__tests__/styles/packaging.test.js +31 -0
  88. package/dist/__tests__/styles/packaging.test.js.map +1 -0
  89. package/dist/__tests__/styles/responsive-layer.test.d.ts +2 -0
  90. package/dist/__tests__/styles/responsive-layer.test.d.ts.map +1 -0
  91. package/dist/__tests__/styles/responsive-layer.test.js +127 -0
  92. package/dist/__tests__/styles/responsive-layer.test.js.map +1 -0
  93. package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
  94. package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
  95. package/dist/__tests__/styles/tokens-drift.test.js +92 -0
  96. package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
  97. package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
  98. package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
  99. package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
  100. package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
  101. package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
  102. package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
  103. package/dist/__tests__/views/canvas-review-mode.render.test.d.ts +2 -0
  104. package/dist/__tests__/views/canvas-review-mode.render.test.d.ts.map +1 -0
  105. package/dist/__tests__/views/canvas-review-mode.render.test.js +70 -0
  106. package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -0
  107. package/dist/__tests__/views/editor-top-bar.render.test.js +30 -1
  108. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  109. package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
  110. package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
  111. package/dist/__tests__/views/page-tab.render.test.js +151 -0
  112. package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
  113. package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
  114. package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
  115. package/dist/__tests__/views/section-class-parity.test.js +81 -0
  116. package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
  117. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
  118. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
  119. package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
  120. package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
  121. package/dist/__tests__/views/version-history-panel.render.test.d.ts +2 -0
  122. package/dist/__tests__/views/version-history-panel.render.test.d.ts.map +1 -0
  123. package/dist/__tests__/views/version-history-panel.render.test.js +153 -0
  124. package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -0
  125. package/dist/components/BreakpointPicker.d.ts +18 -0
  126. package/dist/components/BreakpointPicker.d.ts.map +1 -0
  127. package/dist/components/BreakpointPicker.js +31 -0
  128. package/dist/components/BreakpointPicker.js.map +1 -0
  129. package/dist/components/CanvasEditTip.d.ts +6 -0
  130. package/dist/components/CanvasEditTip.d.ts.map +1 -0
  131. package/dist/components/CanvasEditTip.js +22 -0
  132. package/dist/components/CanvasEditTip.js.map +1 -0
  133. package/dist/components/EditorRail.d.ts +36 -0
  134. package/dist/components/EditorRail.d.ts.map +1 -0
  135. package/dist/components/EditorRail.js +104 -0
  136. package/dist/components/EditorRail.js.map +1 -0
  137. package/dist/components/EditorRailTabs.d.ts +32 -0
  138. package/dist/components/EditorRailTabs.d.ts.map +1 -0
  139. package/dist/components/EditorRailTabs.js +45 -0
  140. package/dist/components/EditorRailTabs.js.map +1 -0
  141. package/dist/components/MediaBlock.d.ts +49 -0
  142. package/dist/components/MediaBlock.d.ts.map +1 -0
  143. package/dist/components/MediaBlock.js +50 -0
  144. package/dist/components/MediaBlock.js.map +1 -0
  145. package/dist/components/ReflowMeter.d.ts +28 -0
  146. package/dist/components/ReflowMeter.d.ts.map +1 -0
  147. package/dist/components/ReflowMeter.js +42 -0
  148. package/dist/components/ReflowMeter.js.map +1 -0
  149. package/dist/components/ZoomChip.d.ts +17 -0
  150. package/dist/components/ZoomChip.d.ts.map +1 -0
  151. package/dist/components/ZoomChip.js +17 -0
  152. package/dist/components/ZoomChip.js.map +1 -0
  153. package/dist/components/breakpoint-icons.d.ts +3 -0
  154. package/dist/components/breakpoint-icons.d.ts.map +1 -0
  155. package/dist/components/breakpoint-icons.js +16 -0
  156. package/dist/components/breakpoint-icons.js.map +1 -0
  157. package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
  158. package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
  159. package/dist/components/page-tab/AuthorPicker.js +95 -0
  160. package/dist/components/page-tab/AuthorPicker.js.map +1 -0
  161. package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
  162. package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
  163. package/dist/components/page-tab/DirectiveSwitch.js +33 -0
  164. package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
  165. package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
  166. package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
  167. package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
  168. package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
  169. package/dist/components/seo/PageAuditPanel.d.ts +14 -0
  170. package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
  171. package/dist/components/seo/PageAuditPanel.js +59 -0
  172. package/dist/components/seo/PageAuditPanel.js.map +1 -0
  173. package/dist/components/seo/SeoCharCount.d.ts +19 -7
  174. package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
  175. package/dist/components/seo/SeoCharCount.js +30 -16
  176. package/dist/components/seo/SeoCharCount.js.map +1 -1
  177. package/dist/components/seo/SeoEditorPane.d.ts +15 -1
  178. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  179. package/dist/components/seo/SeoEditorPane.js +118 -21
  180. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  181. package/dist/components/seo/SerpPreview.d.ts +20 -0
  182. package/dist/components/seo/SerpPreview.d.ts.map +1 -0
  183. package/dist/components/seo/SerpPreview.js +6 -0
  184. package/dist/components/seo/SerpPreview.js.map +1 -0
  185. package/dist/hooks/usePageAudit.d.ts +41 -0
  186. package/dist/hooks/usePageAudit.d.ts.map +1 -0
  187. package/dist/hooks/usePageAudit.js +68 -0
  188. package/dist/hooks/usePageAudit.js.map +1 -0
  189. package/dist/hooks/useReflowGuard.d.ts +47 -0
  190. package/dist/hooks/useReflowGuard.d.ts.map +1 -0
  191. package/dist/hooks/useReflowGuard.js +52 -0
  192. package/dist/hooks/useReflowGuard.js.map +1 -0
  193. package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
  194. package/dist/layout/primitives/AdminShell.js +2 -2
  195. package/dist/layout/primitives/AdminShell.js.map +1 -1
  196. package/dist/lib/breakpoints.d.ts +51 -0
  197. package/dist/lib/breakpoints.d.ts.map +1 -0
  198. package/dist/lib/breakpoints.js +84 -0
  199. package/dist/lib/breakpoints.js.map +1 -0
  200. package/dist/lib/canvas-edit-chrome.d.ts +22 -0
  201. package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
  202. package/dist/lib/canvas-edit-chrome.js +81 -0
  203. package/dist/lib/canvas-edit-chrome.js.map +1 -0
  204. package/dist/lib/media-format.d.ts +55 -0
  205. package/dist/lib/media-format.d.ts.map +1 -0
  206. package/dist/lib/media-format.js +93 -0
  207. package/dist/lib/media-format.js.map +1 -0
  208. package/dist/lib/page-audit-input.d.ts +45 -0
  209. package/dist/lib/page-audit-input.d.ts.map +1 -0
  210. package/dist/lib/page-audit-input.js +53 -0
  211. package/dist/lib/page-audit-input.js.map +1 -0
  212. package/dist/lib/page-editor-service.d.ts +12 -0
  213. package/dist/lib/page-editor-service.d.ts.map +1 -1
  214. package/dist/lib/page-editor-service.js +6 -0
  215. package/dist/lib/page-editor-service.js.map +1 -1
  216. package/dist/lib/page-redirect.d.ts +39 -0
  217. package/dist/lib/page-redirect.d.ts.map +1 -0
  218. package/dist/lib/page-redirect.js +38 -0
  219. package/dist/lib/page-redirect.js.map +1 -0
  220. package/dist/lib/rail-width.d.ts +23 -0
  221. package/dist/lib/rail-width.d.ts.map +1 -0
  222. package/dist/lib/rail-width.js +59 -0
  223. package/dist/lib/rail-width.js.map +1 -0
  224. package/dist/lib/reflow-baseline.d.ts +45 -0
  225. package/dist/lib/reflow-baseline.d.ts.map +1 -0
  226. package/dist/lib/reflow-baseline.js +38 -0
  227. package/dist/lib/reflow-baseline.js.map +1 -0
  228. package/dist/lib/reflow-probe.d.ts +159 -0
  229. package/dist/lib/reflow-probe.d.ts.map +1 -0
  230. package/dist/lib/reflow-probe.js +228 -0
  231. package/dist/lib/reflow-probe.js.map +1 -0
  232. package/dist/lib/seo-service.d.ts +10 -0
  233. package/dist/lib/seo-service.d.ts.map +1 -1
  234. package/dist/lib/seo-service.js.map +1 -1
  235. package/dist/lib/version-diff.d.ts +50 -0
  236. package/dist/lib/version-diff.d.ts.map +1 -0
  237. package/dist/lib/version-diff.js +294 -0
  238. package/dist/lib/version-diff.js.map +1 -0
  239. package/dist/lib/version-history.d.ts +73 -0
  240. package/dist/lib/version-history.d.ts.map +1 -0
  241. package/dist/lib/version-history.js +126 -0
  242. package/dist/lib/version-history.js.map +1 -0
  243. package/dist/prenta-admin.css +1 -1
  244. package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
  245. package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
  246. package/dist/views/page-editor/CanvasMetaLine.js +23 -0
  247. package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
  248. package/dist/views/page-editor/EditorCanvas.d.ts +47 -8
  249. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  250. package/dist/views/page-editor/EditorCanvas.js +68 -34
  251. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  252. package/dist/views/page-editor/EditorTopBar.d.ts +21 -2
  253. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  254. package/dist/views/page-editor/EditorTopBar.js +13 -20
  255. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  256. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  257. package/dist/views/page-editor/PageSectionEditor.js +383 -78
  258. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  259. package/dist/views/page-editor/PageTab.d.ts +36 -0
  260. package/dist/views/page-editor/PageTab.d.ts.map +1 -0
  261. package/dist/views/page-editor/PageTab.js +65 -0
  262. package/dist/views/page-editor/PageTab.js.map +1 -0
  263. package/dist/views/page-editor/ReviewBar.d.ts +21 -0
  264. package/dist/views/page-editor/ReviewBar.d.ts.map +1 -0
  265. package/dist/views/page-editor/ReviewBar.js +22 -0
  266. package/dist/views/page-editor/ReviewBar.js.map +1 -0
  267. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  268. package/dist/views/page-editor/SectionInspector.js +64 -20
  269. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  270. package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
  271. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
  272. package/dist/views/page-editor/SectionsPanel.js +38 -7
  273. package/dist/views/page-editor/SectionsPanel.js.map +1 -1
  274. package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
  275. package/dist/views/page-editor/SitePreviewFrame.js +3 -0
  276. package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
  277. package/dist/views/page-editor/VersionHistoryPanel.d.ts +29 -0
  278. package/dist/views/page-editor/VersionHistoryPanel.d.ts.map +1 -0
  279. package/dist/views/page-editor/VersionHistoryPanel.js +81 -0
  280. package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -0
  281. package/dist/views/page-editor/reflow-context.d.ts +28 -0
  282. package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
  283. package/dist/views/page-editor/reflow-context.js +14 -0
  284. package/dist/views/page-editor/reflow-context.js.map +1 -0
  285. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
  286. package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
  287. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
  288. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
  289. package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
  290. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
  291. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
  292. package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
  293. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
  294. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
  295. package/dist/views/page-editor/sections/GallerySection.js +7 -3
  296. package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
  297. package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
  298. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
  299. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
  300. package/dist/views/page-editor/sections/MissionSection.js +1 -1
  301. package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
  302. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
  303. package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
  304. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
  305. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
  306. package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
  307. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
  308. package/dist/views/page-editor/sections/SectionRenderer.d.ts +13 -1
  309. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  310. package/dist/views/page-editor/sections/SectionRenderer.js +66 -26
  311. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  312. package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
  313. package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
  314. package/dist/views/page-editor/sections/admin-renderers.js +27 -15
  315. package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
  316. package/dist/views/page-editor/sections/parts.d.ts +0 -1
  317. package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
  318. package/dist/views/page-editor/sections/parts.js +41 -16
  319. package/dist/views/page-editor/sections/parts.js.map +1 -1
  320. package/dist/views/page-editor/viewports.d.ts +7 -14
  321. package/dist/views/page-editor/viewports.d.ts.map +1 -1
  322. package/dist/views/page-editor/viewports.js +7 -12
  323. package/dist/views/page-editor/viewports.js.map +1 -1
  324. package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
  325. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  326. package/dist/views/post-editor/PostEditorCanvas.js +5 -2
  327. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  328. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  329. package/dist/views/post-editor/PostSectionEditor.js +2 -20
  330. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  331. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
  332. package/dist/views/post-editor/PostTemplateEditor.js +4 -1
  333. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
  334. package/package.json +5 -3
  335. package/src/AdminRoot.tsx +27 -6
  336. package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
  337. package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
  338. package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
  339. package/src/__tests__/components/media-block.render.test.tsx +126 -0
  340. package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
  341. package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
  342. package/src/__tests__/components/seo-char-count.test.tsx +26 -6
  343. package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
  344. package/src/__tests__/lib/breakpoints.test.ts +84 -0
  345. package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
  346. package/src/__tests__/lib/media-format.test.ts +89 -0
  347. package/src/__tests__/lib/page-audit-input.test.ts +144 -0
  348. package/src/__tests__/lib/page-editor-service.test.ts +2 -0
  349. package/src/__tests__/lib/page-redirect.test.ts +29 -0
  350. package/src/__tests__/lib/rail-width.test.ts +86 -0
  351. package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
  352. package/src/__tests__/lib/reflow-probe.test.ts +95 -0
  353. package/src/__tests__/lib/version-diff.test.ts +278 -0
  354. package/src/__tests__/lib/version-history.test.ts +166 -0
  355. package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
  356. package/src/__tests__/styles/packaging.test.ts +38 -0
  357. package/src/__tests__/styles/responsive-layer.test.ts +152 -0
  358. package/src/__tests__/styles/tokens-drift.test.ts +110 -0
  359. package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
  360. package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
  361. package/src/__tests__/views/canvas-review-mode.render.test.tsx +94 -0
  362. package/src/__tests__/views/editor-top-bar.render.test.tsx +43 -1
  363. package/src/__tests__/views/page-tab.render.test.tsx +226 -0
  364. package/src/__tests__/views/section-class-parity.test.ts +98 -0
  365. package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
  366. package/src/__tests__/views/version-history-panel.render.test.tsx +195 -0
  367. package/src/components/BreakpointPicker.tsx +114 -0
  368. package/src/components/CanvasEditTip.tsx +46 -0
  369. package/src/components/EditorRail.tsx +219 -0
  370. package/src/components/EditorRailTabs.tsx +117 -0
  371. package/src/components/MediaBlock.tsx +223 -0
  372. package/src/components/ReflowMeter.tsx +125 -0
  373. package/src/components/ZoomChip.tsx +39 -0
  374. package/src/components/breakpoint-icons.ts +17 -0
  375. package/src/components/page-tab/AuthorPicker.tsx +240 -0
  376. package/src/components/page-tab/DirectiveSwitch.tsx +89 -0
  377. package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
  378. package/src/components/seo/PageAuditPanel.tsx +192 -0
  379. package/src/components/seo/SeoCharCount.tsx +55 -24
  380. package/src/components/seo/SeoEditorPane.tsx +213 -31
  381. package/src/components/seo/SerpPreview.tsx +41 -0
  382. package/src/hooks/usePageAudit.ts +98 -0
  383. package/src/hooks/useReflowGuard.ts +96 -0
  384. package/src/layout/primitives/AdminShell.tsx +13 -1
  385. package/src/lib/breakpoints.ts +107 -0
  386. package/src/lib/canvas-edit-chrome.ts +82 -0
  387. package/src/lib/media-format.ts +99 -0
  388. package/src/lib/page-audit-input.ts +104 -0
  389. package/src/lib/page-editor-service.ts +18 -0
  390. package/src/lib/page-redirect.ts +60 -0
  391. package/src/lib/rail-width.ts +58 -0
  392. package/src/lib/reflow-baseline.ts +70 -0
  393. package/src/lib/reflow-probe.ts +299 -0
  394. package/src/lib/seo-service.ts +10 -0
  395. package/src/lib/version-diff.ts +376 -0
  396. package/src/lib/version-history.ts +192 -0
  397. package/src/styles/prenta-tokens.css +120 -42
  398. package/src/styles/responsive.css +281 -0
  399. package/src/styles/theme.css +138 -0
  400. package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
  401. package/src/views/page-editor/EditorCanvas.tsx +140 -54
  402. package/src/views/page-editor/EditorTopBar.tsx +55 -51
  403. package/src/views/page-editor/PageSectionEditor.tsx +575 -185
  404. package/src/views/page-editor/PageTab.tsx +433 -0
  405. package/src/views/page-editor/ReviewBar.tsx +76 -0
  406. package/src/views/page-editor/SectionInspector.tsx +84 -42
  407. package/src/views/page-editor/SectionsPanel.tsx +282 -141
  408. package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
  409. package/src/views/page-editor/VersionHistoryPanel.tsx +302 -0
  410. package/src/views/page-editor/reflow-context.ts +43 -0
  411. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
  412. package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
  413. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
  414. package/src/views/page-editor/sections/GallerySection.tsx +7 -3
  415. package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
  416. package/src/views/page-editor/sections/MissionSection.tsx +4 -2
  417. package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
  418. package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
  419. package/src/views/page-editor/sections/SectionRenderer.tsx +115 -34
  420. package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
  421. package/src/views/page-editor/sections/parts.tsx +46 -18
  422. package/src/views/page-editor/viewports.ts +13 -26
  423. package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
  424. package/src/views/post-editor/PostSectionEditor.tsx +1 -56
  425. package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
@@ -6,21 +6,24 @@ import { toast } from 'sonner'
6
6
  import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
7
7
  import { useNavigationBlocker } from '../../router/index.js'
8
8
  import { SchedulePublishDialog } from '../../components/SchedulePublishDialog.js'
9
- import { EditorSeoAside } from '../../components/EditorSeoAside.js'
9
+ import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
10
+ import { EditorRail, RailHeader } from '../../components/EditorRail.js'
11
+ import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
10
12
  import { ErrorBoundary } from '../../components/ErrorBoundary.js'
11
- import { VersionHistory } from '../../components/VersionHistory.js'
13
+ import { VersionHistoryPanel } from './VersionHistoryPanel.js'
14
+ import { ReviewBar } from './ReviewBar.js'
15
+ import {
16
+ fetchPageVersions,
17
+ restorePageVersion,
18
+ type PageVersion,
19
+ } from '../../lib/version-history.js'
20
+ import { changedSectionIds, diffVersions } from '../../lib/version-diff.js'
12
21
  import {
13
- addSection,
14
- createSection,
15
- duplicateSection,
16
22
  emptyPage,
17
23
  fetchPageForEditor,
18
24
  missingPageFields,
19
- moveSection,
20
25
  publishPage,
21
26
  PAGES_COLLECTION,
22
- removeSection,
23
- reorderSections,
24
27
  savePageDraft,
25
28
  toggleSectionVisibility,
26
29
  updateSectionContent,
@@ -38,20 +41,33 @@ import {
38
41
  createPreviewToken,
39
42
  } from '../../lib/preview-link.js'
40
43
  import { usePublicSiteUrl } from '../../hooks/usePublicSiteUrl.js'
41
- import type { SectionSettings, SectionTypeId } from './section-types.js'
44
+ import type { SectionSettings } from './section-types.js'
42
45
  import { DEFAULT_VIEWPORT, type ViewportId } from './viewports.js'
46
+ import type { Zoom } from '../../lib/breakpoints.js'
47
+ import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
48
+ import {
49
+ DEFAULT_DIRECTIVES,
50
+ directivesFromData,
51
+ type RobotsPermissions,
52
+ } from '@prenta/core/seo/robots-directives'
53
+ import { createPublishRedirect, pendingSlugChange } from '../../lib/page-redirect.js'
54
+ import { updateSeoFields } from '../../lib/seo-service.js'
55
+ import { PageTab } from './PageTab.js'
56
+ import { duplicatePage, deletePage } from '../../lib/pages-service.js'
57
+ import { scoreBand, type PageAuditResult } from '@prenta/core/seo/page-audit'
58
+ import { usePageAudit } from '../../hooks/usePageAudit.js'
43
59
  import { hasCanvasRenderer, useAdminSectionRenderers } from './sections/index.js'
44
60
  import { EditorTopBar, type SaveState } from './EditorTopBar.js'
45
- import { SectionsPanel } from './SectionsPanel.js'
61
+ import { SectionsList } from './SectionsPanel.js'
46
62
  import { EditorCanvas } from './EditorCanvas.js'
63
+ import { ReflowContext } from './reflow-context.js'
47
64
  import { SitePreviewFrame } from './SitePreviewFrame.js'
48
65
  import { useSitePreview } from './useSitePreview.js'
49
66
  import { SectionInspector } from './SectionInspector.js'
50
- import { AddSectionModal } from './AddSectionModal.js'
51
67
  import { PageSettingsModal } from './PageSettingsModal.js'
52
68
  import { ValidationSummary } from './ValidationSummary.js'
53
69
  import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
54
- import { computeLiveSeoScore } from '../../lib/editor-seo-content.js'
70
+ import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
55
71
  import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
56
72
 
57
73
  interface PageSectionEditorProps {
@@ -74,16 +90,47 @@ interface PageSectionEditorProps {
74
90
  onOpenPreview?: (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => void
75
91
  }
76
92
 
77
- function deriveCan(session: any): { canEdit: boolean; canPublish: boolean } {
93
+ /**
94
+ * The lower of the two audit scores — the tab dot should reflect the half that
95
+ * needs work, not an average that hides it.
96
+ *
97
+ * Three-valued on purpose, and it must NOT fall back to another scorer:
98
+ * undefined — the audit has not reported yet (the SEO tab has never been
99
+ * opened). No dot beats a dot from a different model.
100
+ * null — both groups are gated off, so there is genuinely nothing to
101
+ * rate. An earlier version fell back to `computeLiveSeoScore`
102
+ * here, which put a number from the content analyser — different
103
+ * model, different bands — on a tab summarising the audit. That
104
+ * is the disagreement this wiring exists to remove.
105
+ */
106
+ function weakestAuditScore(audit: PageAuditResult | null): number | null | undefined {
107
+ if (!audit) return undefined
108
+ const scores = [audit.seo.score, audit.geo.score].filter((s): s is number => s !== null)
109
+ return scores.length ? Math.min(...scores) : null
110
+ }
111
+
112
+ function deriveCan(session: any): {
113
+ canEdit: boolean
114
+ canPublish: boolean
115
+ canCreateRedirect: boolean
116
+ } {
78
117
  const role: string | undefined = session?.user?.role
79
118
  // Default closed: when we can't identify a role, hide the edit/publish
80
119
  // affordances rather than exposing them. The server independently enforces
81
120
  // access on every write, so this is a UX guard — but it should fail safe.
82
121
  // (Matches the Post editor's behavior.)
83
- if (!role) return { canEdit: false, canPublish: false }
122
+ if (!role) return { canEdit: false, canPublish: false, canCreateRedirect: false }
84
123
  const editors = ['ADMIN', 'EDITOR', 'AUTHOR']
85
124
  const publishers = ['ADMIN', 'EDITOR']
86
- return { canEdit: editors.includes(role), canPublish: publishers.includes(role) }
125
+ // Same roles that may publish a page may create the 301 that follows a
126
+ // slug move. Offering the checkbox to anyone else promises a redirect
127
+ // `POST /redirects` will reject.
128
+ const redirectCreators = ['OWNER', 'ADMIN', 'EDITOR']
129
+ return {
130
+ canEdit: editors.includes(role),
131
+ canPublish: publishers.includes(role),
132
+ canCreateRedirect: redirectCreators.includes(role),
133
+ }
87
134
  }
88
135
 
89
136
  export function PageSectionEditor({
@@ -109,21 +156,90 @@ export function PageSectionEditor({
109
156
  fieldPath: string
110
157
  } | null>(null)
111
158
  const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
159
+ const [zoom, setZoom] = useState<Zoom>('fit')
160
+ /**
161
+ * Live audit reported by the SEO pane, built from unsaved form state. Null
162
+ * until that pane has mounted — `audit` below falls back to the saved-record
163
+ * audit so the header chip and tab dot always have a number.
164
+ */
165
+ const [liveAudit, setLiveAudit] = useState<PageAuditResult | null>(null)
166
+ /** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
167
+ const [canvasScale, setCanvasScale] = useState(1)
168
+ /**
169
+ * The live canvas surface, for the reflow guard to clone. State rather than a
170
+ * ref because the context value has to update when it mounts — the canvas
171
+ * unmounts whenever the site-preview frame takes over.
172
+ */
173
+ const [canvasEl, setCanvasEl] = useState<HTMLElement | null>(null)
174
+ /**
175
+ * Section content as it is on the live site. Captured whenever the editor
176
+ * takes delivery of a PUBLISHED page — on load, and after each save and
177
+ * publish, both of which write straight through to the live content.
178
+ */
179
+ const [baseline, setBaseline] = useState<ReflowBaseline | null>(null)
180
+ /**
181
+ * Path as it is on the live site, captured with the reflow baseline and for
182
+ * the same reason: a slug change only breaks links if something is already
183
+ * live at the old address.
184
+ */
185
+ const [publishedPath, setPublishedPath] = useState<string | null>(null)
186
+ const [directives, setDirectives] = useState<RobotsPermissions>(DEFAULT_DIRECTIVES)
187
+ /**
188
+ * Defaults ON. The editor changing a live page's slug almost always wants the
189
+ * redirect, and the cost of an unwanted one is a rule they can delete, while
190
+ * the cost of a missing one is 404s discovered by visitors.
191
+ */
192
+ const [redirectOnPublish, setRedirectOnPublish] = useState(true)
193
+
194
+ // The reflow guard's inputs, gathered in one place. Memoised because a new
195
+ // object identity on every render would restart the debounce in every guarded
196
+ // field's effect — the measurement would then never settle while typing,
197
+ // which is precisely when it is wanted.
198
+ const reflowContext = useMemo(
199
+ () => ({
200
+ canvas: canvasEl,
201
+ sectionId: selectedId,
202
+ baseline,
203
+ device: viewport,
204
+ onProof: setViewport,
205
+ }),
206
+ [canvasEl, selectedId, baseline, viewport],
207
+ )
208
+
209
+ /**
210
+ * Take delivery of server state.
211
+ *
212
+ * Every path that receives a fresh page from the API goes through here so the
213
+ * reflow baseline is re-captured with it. Saving a PUBLISHED page writes
214
+ * straight through to the live content — there is no draft/published split —
215
+ * so a save moves the baseline exactly as a publish does. Missing one would
216
+ * leave the guard comparing against content that is no longer on the site.
217
+ */
218
+ const acceptPage = useCallback((p: EditorPage) => {
219
+ setPage(p)
220
+ setBaseline(snapshotBaseline(p.sections, p.status))
221
+ setPublishedPath(p.status === 'PUBLISHED' ? p.path : null)
222
+ }, [])
112
223
  const [dirty, setDirty] = useState(false)
113
224
  const [saveState, setSaveState] = useState<SaveState>('idle')
114
225
  const [publishing, setPublishing] = useState(false)
115
226
 
116
- const [addOpen, setAddOpen] = useState(false)
117
227
  const [settingsOpen, setSettingsOpen] = useState(false)
118
- const [seoOpen, setSeoOpen] = useState(false)
228
+ /** Active rail tab. Replaces the old boolean `seoOpen` right-hand aside. */
229
+ const [railTab, setRailTab] = useState<RailTabId>('sections')
119
230
  const [historyOpen, setHistoryOpen] = useState(false)
120
- const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
231
+ const [versions, setVersions] = useState<PageVersion[]>([])
232
+ const [versionsLoading, setVersionsLoading] = useState(false)
233
+ const [versionsError, setVersionsError] = useState<string | null>(null)
234
+ /** Index into `versions`; 0 is the newest, which is the current content. */
235
+ const [versionIndex, setVersionIndex] = useState(0)
236
+ const [restoring, setRestoring] = useState(false)
121
237
  const [confirmPublish, setConfirmPublish] = useState(false)
122
238
  const [scheduleOpen, setScheduleOpen] = useState(false)
123
239
  const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
124
240
  const [validation, setValidation] = useState<ValidationResult | null>(null)
125
241
 
126
- const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
242
+ const { canEdit, canPublish, canCreateRedirect } = useMemo(() => deriveCan(session), [session])
127
243
  const customRenderers = useAdminSectionRenderers()
128
244
  const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
129
245
 
@@ -148,18 +264,179 @@ export function PageSectionEditor({
148
264
  )
149
265
  const publishReady = Boolean(page) && publishCheck.errors.length === 0
150
266
 
151
- const liveSeoScore = useMemo(() => {
152
- if (!page) return 0
153
- return computeLiveSeoScore({
154
- title: page.title,
155
- slug: page.slug,
156
- seoTitle: page.seoTitle,
157
- seoDescription: page.seoDescription,
158
- sections: page.sections,
159
- })
160
- }, [page])
161
267
  const publishDisabledReason = publishCheck.errors[0]
162
268
 
269
+ // Saved-record audit, available from first paint. Both this and the pane's
270
+ // live audit go through `buildPageAuditInput`, so they can differ only in
271
+ // saved-versus-unsaved metadata — never in how a check is decided.
272
+ const {
273
+ audit: savedAudit,
274
+ record: seoRecord,
275
+ reload: reloadAudit,
276
+ } = usePageAudit({
277
+ entityType: 'page',
278
+ entityId: page?.id ?? null,
279
+ sections: page?.sections ?? [],
280
+ slug: page?.slug ?? '',
281
+ fallback: { title: page?.seoTitle, description: page?.seoDescription },
282
+ })
283
+
284
+ // Seed directives from the saved record. Keyed on the record's identity, not
285
+ // on its fields — re-running on every field change would discard whatever the
286
+ // editor had just toggled and not yet saved.
287
+ useEffect(() => {
288
+ if (!seoRecord) return
289
+ setDirectives(
290
+ directivesFromData({
291
+ noindex: seoRecord.noindex,
292
+ nofollow: seoRecord.nofollow,
293
+ nosnippet: seoRecord.nosnippet,
294
+ noarchive: seoRecord.noarchive,
295
+ maxImagePreview: seoRecord.maxImagePreview,
296
+ }),
297
+ )
298
+ }, [seoRecord?.id])
299
+
300
+ // Unsaved edits win: while the SEO pane is open its number is the truthful
301
+ // one, and every surface reads this single value.
302
+ const audit = liveAudit ?? savedAudit
303
+
304
+ // Content the SEO pane analyses. Hoisted out of the old aside wrapper so the
305
+ // rail can render the pane directly — the aside only ever existed to supply
306
+ // a resizable container, which the rail now is.
307
+ const seoContentHtml = useMemo(
308
+ () => buildEditorContentHtml(page?.sections ?? []),
309
+ [page?.sections],
310
+ )
311
+
312
+ const railTabs = useMemo<RailTab[]>(
313
+ () => [
314
+ {
315
+ id: 'sections',
316
+ label: 'Sections',
317
+ title: 'Page content',
318
+ count: page?.sections.length ?? 0,
319
+ },
320
+ {
321
+ id: 'seo',
322
+ label: 'SEO & GEO',
323
+ title: 'Search & AI readiness',
324
+ // The weaker of the two: a page can be perfectly crawlable and still
325
+ // invisible to answer engines, and a dot that averaged them would hide
326
+ // whichever half is failing. `null` means a gate is off, which the tab
327
+ // reports as unrated rather than as a low score.
328
+ score: weakestAuditScore(audit),
329
+ },
330
+ {
331
+ id: 'page',
332
+ label: 'Page',
333
+ title: 'Page settings',
334
+ },
335
+ ],
336
+ [page?.sections.length, audit],
337
+ )
338
+
339
+ const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0]!
340
+
341
+ // ─── Version history ─────────────────────────────────────────────────
342
+ // History is a MODE, not a dialog: it overlays the rail and drives the
343
+ // canvas, so the version being reviewed is editor state rather than
344
+ // something owned by a panel that can be unmounted.
345
+
346
+ const loadVersions = useCallback(async (pageId: string) => {
347
+ setVersionsLoading(true)
348
+ setVersionsError(null)
349
+ try {
350
+ setVersions(await fetchPageVersions(PAGES_COLLECTION, pageId))
351
+ } catch (err) {
352
+ setVersions([])
353
+ setVersionsError(err instanceof Error ? err.message : 'Failed to load version history')
354
+ } finally {
355
+ setVersionsLoading(false)
356
+ }
357
+ }, [])
358
+
359
+ const openHistory = useCallback(() => {
360
+ if (!page?.id) return
361
+ setHistoryOpen(true)
362
+ // Always land on "current" so opening history never silently swaps the
363
+ // canvas out from under the editor.
364
+ setVersionIndex(0)
365
+ void loadVersions(page.id)
366
+ }, [page?.id, loadVersions])
367
+
368
+ const closeHistory = useCallback(() => {
369
+ setHistoryOpen(false)
370
+ setVersionIndex(0)
371
+ }, [])
372
+
373
+ /**
374
+ * Index 0 is the current content, which the canvas is already showing — so
375
+ * selecting it is "back to current", not a review. Only an older index puts
376
+ * the editor into review mode.
377
+ */
378
+ const reviewing = historyOpen && versionIndex > 0
379
+ const reviewVersion = reviewing ? (versions[versionIndex] ?? null) : null
380
+
381
+ // What that version's save CHANGED — diffed against the one before it, which
382
+ // is the question the outlines on the canvas answer.
383
+ const reviewChanges = useMemo(
384
+ () =>
385
+ reviewVersion
386
+ ? diffVersions(reviewVersion.sections, versions[versionIndex + 1]?.sections)
387
+ : [],
388
+ [reviewVersion, versions, versionIndex],
389
+ )
390
+ const reviewChangedIds = useMemo(() => changedSectionIds(reviewChanges), [reviewChanges])
391
+
392
+ const handleRestoreVersion = useCallback(
393
+ async (version: PageVersion) => {
394
+ if (!page?.id) return
395
+ setRestoring(true)
396
+ try {
397
+ await restorePageVersion(PAGES_COLLECTION, page.id, version.id)
398
+ // Re-read rather than trusting the snapshot we already hold: the
399
+ // restore goes through the normal update path, so hooks and derived
400
+ // fields may have altered what actually landed.
401
+ const restored = await fetchPageForEditor(page.id)
402
+ acceptPage(restored)
403
+ setSelectedId(null)
404
+ setDirty(false)
405
+ setSaveState('idle')
406
+ setValidation(null)
407
+ closeHistory()
408
+ void loadVersions(page.id)
409
+ toast.success('Version restored')
410
+ } catch (err) {
411
+ toast.error(err instanceof Error ? err.message : 'Failed to restore version')
412
+ } finally {
413
+ setRestoring(false)
414
+ }
415
+ },
416
+ [page?.id, acceptPage, closeHistory, loadVersions],
417
+ )
418
+
419
+ // ─── Edit permission ─────────────────────────────────────────────────
420
+ /**
421
+ * ADR 0002: a Page-Builder-owned page (it has a `_layout`) compiles its
422
+ * sections from the builder layout on every save, so editing them here would
423
+ * be overwritten. The server independently refuses such edits with 409.
424
+ */
425
+ const readOnly = page?.builderOwned ?? false
426
+ /**
427
+ * The single gate every writing surface reads.
428
+ *
429
+ * Hoisted above the callbacks rather than derived at render time, because
430
+ * the two that were left behind were the ones that mattered: the site
431
+ * preview's in-place edit handlers close over `canEdit` (the raw role) and
432
+ * so kept accepting edits while a past version was under review — and while
433
+ * a builder-owned page was supposedly read-only. The iframe is a second
434
+ * editing surface, and a lock that only covers the rail is not a lock.
435
+ */
436
+ const effectiveCanEdit = canEdit && !readOnly && !reviewing
437
+ /** Restoring is not editing: it is allowed precisely while reviewing. */
438
+ const canRestoreVersion = canEdit && !readOnly
439
+
163
440
  // ─── Load ──────────────────────────────────────────────────────────
164
441
  useEffect(() => {
165
442
  let cancelled = false
@@ -175,7 +452,7 @@ export function PageSectionEditor({
175
452
  }
176
453
 
177
454
  if (!documentId) {
178
- setPage(emptyPage(initialParentId ?? null))
455
+ acceptPage(emptyPage(initialParentId ?? null))
179
456
  setLoading(false)
180
457
  // Start a brand-new page the way WordPress does: name it first. The
181
458
  // settings modal opens with the title focused and the slug/path
@@ -187,7 +464,7 @@ export function PageSectionEditor({
187
464
  fetchPageForEditor(documentId)
188
465
  .then((p) => {
189
466
  if (cancelled) return
190
- setPage(p)
467
+ acceptPage(p)
191
468
  // Default to a clean canvas (no selection / no inspector) to match
192
469
  // the editor's default composition; the inspector opens only once
193
470
  // the user picks a section to edit.
@@ -218,38 +495,11 @@ export function PageSectionEditor({
218
495
  [],
219
496
  )
220
497
 
221
- const handleAdd = useCallback(
222
- (type: SectionTypeId) => {
223
- const section = createSection(type)
224
- mutateSections((s) => addSection(s, section))
225
- setSelectedId(section.id)
226
- setAddOpen(false)
227
- },
228
- [mutateSections],
229
- )
230
-
231
- const handleReorder = useCallback(
232
- (orderedIds: string[]) => mutateSections((s) => reorderSections(s, orderedIds)),
233
- [mutateSections],
234
- )
235
-
236
498
  const handleToggleVisibility = useCallback(
237
499
  (id: string) => mutateSections((s) => toggleSectionVisibility(s, id)),
238
500
  [mutateSections],
239
501
  )
240
502
 
241
- const handleDuplicate = useCallback(
242
- (id: string) => mutateSections((s) => duplicateSection(s, id)),
243
- [mutateSections],
244
- )
245
-
246
- const confirmDelete = useCallback(() => {
247
- if (!deleteTargetId) return
248
- mutateSections((s) => removeSection(s, deleteTargetId))
249
- setSelectedId((cur) => (cur === deleteTargetId ? null : cur))
250
- setDeleteTargetId(null)
251
- }, [deleteTargetId, mutateSections])
252
-
253
503
  const handleContentChange = useCallback(
254
504
  (patch: Record<string, unknown>) => {
255
505
  if (!selectedId) return
@@ -282,6 +532,35 @@ export function PageSectionEditor({
282
532
  }, [])
283
533
 
284
534
  // ─── Save / publish ──────────────────────────────────────────────────
535
+ /**
536
+ * Persist robots directives. They live on the SEO record, not the document
537
+ * body, so they need their own write — and it has to happen on both save and
538
+ * publish, or a toggle would appear to stick until the next reload.
539
+ *
540
+ * Reported on failure rather than swallowed: a directive that silently failed
541
+ * to save is the same defect as one that never reached the head.
542
+ */
543
+ const saveDirectives = useCallback(
544
+ async (pageId: string) => {
545
+ const { error } = await updateSeoFields('page', pageId, {
546
+ noindex: !directives.index,
547
+ nofollow: !directives.follow,
548
+ nosnippet: !directives.snippet,
549
+ noarchive: !directives.archive,
550
+ maxImagePreview: directives.maxImagePreview,
551
+ })
552
+ if (error) {
553
+ toast.error(`Search directives were not saved: ${error}`)
554
+ return false
555
+ }
556
+ // The header chip and rail dot read the saved SEO record. Without a
557
+ // reload they keep the pre-save score after noindex/nosnippet flips.
558
+ await reloadAudit()
559
+ return true
560
+ },
561
+ [directives, reloadAudit],
562
+ )
563
+
285
564
  const handleSaveDraft = useCallback(async (): Promise<boolean> => {
286
565
  if (!page) return false
287
566
  const result = validatePage(page, false)
@@ -294,7 +573,8 @@ export function PageSectionEditor({
294
573
  setSaveState('saving')
295
574
  try {
296
575
  const saved = await savePageDraft(page)
297
- setPage(saved)
576
+ if (saved.id) await saveDirectives(saved.id)
577
+ acceptPage(saved)
298
578
  setDirty(false)
299
579
  setSaveState('saved')
300
580
  setValidation(null)
@@ -306,7 +586,7 @@ export function PageSectionEditor({
306
586
  toast.error(err instanceof Error ? err.message : 'Failed to save draft')
307
587
  return false
308
588
  }
309
- }, [page, documentId, onNavigate])
589
+ }, [page, documentId, onNavigate, saveDirectives, acceptPage])
310
590
 
311
591
  // Inline branded preview — Next builder canvas (same origin as admin), not
312
592
  // the Astro public apex. "View on site" still uses publicSiteUrl below.
@@ -375,8 +655,27 @@ export function PageSectionEditor({
375
655
  if (!page) return
376
656
  setPublishing(true)
377
657
  try {
658
+ // The move has to be read BEFORE publishing — `acceptPage` moves the
659
+ // published path to the new one, after which there is nothing to detect.
660
+ const move =
661
+ redirectOnPublish && canCreateRedirect ? pendingSlugChange(publishedPath, page.path) : null
662
+
378
663
  const published = await publishPage(page)
379
- setPage(published)
664
+ if (published.id) await saveDirectives(published.id)
665
+
666
+ if (move) {
667
+ const { error } = await createPublishRedirect(move)
668
+ if (error) {
669
+ // Loud, and naming both paths: the page has already moved, so a
670
+ // silently-failed redirect leaves exactly the broken links the switch
671
+ // promised to prevent.
672
+ toast.error(`Published, but the redirect ${move.from} → ${move.to} failed: ${error}`)
673
+ } else {
674
+ toast.success(`Redirect created: ${move.from} → ${move.to}`)
675
+ }
676
+ }
677
+
678
+ acceptPage(published)
380
679
  setDirty(false)
381
680
  setSaveState('saved')
382
681
  setValidation(null)
@@ -387,67 +686,92 @@ export function PageSectionEditor({
387
686
  } finally {
388
687
  setPublishing(false)
389
688
  }
390
- }, [page, documentId, onNavigate])
391
-
392
- // After a version restore the server has already written the restored
393
- // data — refetch so the editor reflects it instead of the stale state.
394
- const handleRestored = useCallback(() => {
689
+ }, [
690
+ page,
691
+ documentId,
692
+ onNavigate,
693
+ saveDirectives,
694
+ redirectOnPublish,
695
+ canCreateRedirect,
696
+ publishedPath,
697
+ acceptPage,
698
+ ])
699
+
700
+ const handleDuplicate = useCallback(async () => {
395
701
  if (!page?.id) return
396
- fetchPageForEditor(page.id)
397
- .then((p) => {
398
- setPage(p)
399
- setSelectedId(null)
400
- setSeoOpen(false)
401
- setDirty(false)
402
- setSaveState('idle')
403
- setValidation(null)
404
- })
405
- .catch(() => {
406
- toast.error('Version restored — refresh the page to see it.')
407
- })
408
- }, [page?.id])
702
+ const { id, error } = await duplicatePage(page.id)
703
+ if (error || !id) {
704
+ toast.error(error ?? 'Could not duplicate the page')
705
+ return
706
+ }
707
+ toast.success('Page duplicated')
708
+ // Navigate to the copy rather than staying put. The duplicate is a draft
709
+ // named "(Copy)", and leaving the editor on the original makes it look as
710
+ // though nothing happened.
711
+ onNavigate(`/pages/${id}/edit`)
712
+ }, [page?.id, onNavigate])
713
+
714
+ const handleTrash = useCallback(async () => {
715
+ if (!page?.id) return
716
+ const { error } = await deletePage(page.id)
717
+ if (error) {
718
+ toast.error(error)
719
+ return
720
+ }
721
+ toast.success('Page moved to trash')
722
+ onNavigate('/pages')
723
+ }, [page?.id, onNavigate])
409
724
 
410
725
  const handleSeoSaved = useCallback(() => {
726
+ reloadAudit()
411
727
  if (!page?.id) return
412
728
  fetchPageForEditor(page.id)
413
- .then((p) => setPage(p))
729
+ .then((p) => acceptPage(p))
414
730
  .catch(() => {
415
731
  /* SEO API saved; editor meta will sync on next full load */
416
732
  })
417
- }, [page?.id])
733
+ }, [page?.id, reloadAudit])
418
734
 
735
+ // The header's SEO chip is a shortcut to the rail's SEO tab. Toggling back
736
+ // returns to Sections rather than leaving the rail on a tab the chip no
737
+ // longer appears to be showing.
419
738
  const handleToggleSeo = useCallback(() => {
420
- setSeoOpen((open) => {
421
- if (!open) setSelectedId(null)
422
- return !open
739
+ setRailTab((tab) => {
740
+ if (tab !== 'seo') setSelectedId(null)
741
+ return tab === 'seo' ? 'sections' : 'seo'
423
742
  })
424
743
  }, [])
425
744
 
426
745
  const handleSelectSection = useCallback((id: string | null, fieldPath?: string) => {
427
- setSeoOpen(false)
746
+ // Selecting on the canvas is a request to edit that section — show the
747
+ // outline it belongs to, whichever tab was open.
748
+ setRailTab('sections')
428
749
  setSelectedId(id)
429
750
  setPendingFocusPath(fieldPath ?? null)
430
751
  }, [])
431
752
 
753
+ // `effectiveCanEdit`, not `canEdit`: the site preview is a second editing
754
+ // surface, so a role check alone let it write through both the version-review
755
+ // lock and the builder-owned lock the rest of the editor honours.
432
756
  const handleCanvasEdit = useCallback(
433
757
  (edit: { sectionId: string; fieldPath: string; value: string }) => {
434
- if (!canEdit) return
758
+ if (!effectiveCanEdit) return
435
759
  setSuppressSectionsEcho((n) => n + 1)
436
760
  mutateSections((s) =>
437
761
  updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value),
438
762
  )
439
763
  setSelectedId(edit.sectionId)
440
764
  },
441
- [canEdit, mutateSections],
765
+ [effectiveCanEdit, mutateSections],
442
766
  )
443
767
 
444
768
  const handleCanvasRequestMedia = useCallback(
445
769
  (request: { sectionId: string; fieldPath: string }) => {
446
- if (!canEdit) return
770
+ if (!effectiveCanEdit) return
447
771
  setSelectedId(request.sectionId)
448
772
  setCanvasMediaTarget(request)
449
773
  },
450
- [canEdit],
774
+ [effectiveCanEdit],
451
775
  )
452
776
 
453
777
  // After a canvas field click, focus the matching inspector input once painted.
@@ -580,12 +904,12 @@ export function PageSectionEditor({
580
904
 
581
905
  const selected = page.sections.find((s) => s.id === selectedId) ?? null
582
906
 
583
- // ADR 0002: a Page-Builder-owned page (it has a `_layout`) compiles its
584
- // sections from the builder layout on every save, so editing them here would
585
- // be overwritten. Open this editor read-only and route the user to the
586
- // builder. The server independently refuses such edits with 409.
587
- const readOnly = page.builderOwned
588
- const effectiveCanEdit = canEdit && !readOnly
907
+ // `readOnly` / `effectiveCanEdit` / `canRestoreVersion` are computed above,
908
+ // with the hooks, so the callbacks defined there share this one gate.
909
+
910
+ // The canvas shows the reviewed snapshot, but `page.sections` stays the
911
+ // editor's own content throughout — nothing about review mode writes to it.
912
+ const canvasSections = reviewVersion ? reviewVersion.sections : page.sections
589
913
 
590
914
  // Sections whose type has no registered definition (seed/code-owned, e.g.
591
915
  // `opal-*`). They are preserved verbatim on save but can't be edited here.
@@ -620,16 +944,28 @@ export function PageSectionEditor({
620
944
  publishDisabledReason={publishDisabledReason}
621
945
  viewport={viewport}
622
946
  onViewport={setViewport}
947
+ {...(sitePreview.active
948
+ ? // The site preview is an iframe of the real front end, not a
949
+ // scaled canvas — a zoom percentage there would be a lie.
950
+ {}
951
+ : { zoom, canvasScale, onZoom: setZoom })}
623
952
  onBack={() => guardedNavigate('/pages')}
624
953
  onPreview={handlePreview}
625
954
  onViewOnSite={page.id ? handleViewOnSite : undefined}
626
955
  sitePreview={sitePreview.active}
627
956
  onToggleSitePreview={page.id && page.slug ? sitePreview.toggle : undefined}
628
- onHistory={page.id ? () => setHistoryOpen(true) : undefined}
957
+ historyOpen={historyOpen}
958
+ onHistory={page.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined}
629
959
  onOpenSettings={() => setSettingsOpen(true)}
630
960
  onOpenSeo={handleToggleSeo}
631
- seoOpen={seoOpen}
632
- seoScore={liveSeoScore}
961
+ seoOpen={railTab === 'seo'}
962
+ // The audit's SEO score, banded by the audit's own scale. Previously
963
+ // this chip showed `computeLiveSeoScore` — a different model — so the
964
+ // header read 53 while the rings beside it read 0 and 57.
965
+ seoScore={audit?.seo.score ?? undefined}
966
+ seoScoreBand={
967
+ typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined
968
+ }
633
969
  onSaveDraft={handleSaveDraft}
634
970
  onPublish={requestPublish}
635
971
  onSchedule={page.id ? requestSchedule : undefined}
@@ -687,88 +1023,152 @@ export function PageSectionEditor({
687
1023
  />
688
1024
  )}
689
1025
 
690
- <div className="relative flex flex-1 overflow-hidden">
691
- <SectionsPanel
692
- sections={page.sections}
693
- selectedId={selectedId}
694
- canEdit={effectiveCanEdit}
695
- onSelect={handleSelectSection}
696
- onReorder={handleReorder}
697
- onToggleVisibility={handleToggleVisibility}
698
- onDuplicate={handleDuplicate}
699
- onRequestDelete={(id) => setDeleteTargetId(id)}
700
- onAdd={() => setAddOpen(true)}
701
- />
702
-
703
- <div className="flex-1 overflow-hidden">
704
- {sitePreview.active ? (
705
- <SitePreviewFrame
706
- src={sitePreview.url}
707
- viewport={viewport}
708
- reloadKey={sitePreview.reloadKey}
709
- title={`Site preview: ${page.title || 'Untitled page'}`}
710
- selectedId={selectedId}
711
- sections={page.sections}
712
- suppressSectionsEcho={suppressSectionsEcho}
713
- onSelectSection={handleSelectSection}
714
- onCanvasEdit={handleCanvasEdit}
715
- onCanvasRequestMedia={handleCanvasRequestMedia}
716
- />
717
- ) : (
718
- <EditorCanvas
719
- sections={page.sections}
720
- selectedId={selectedId}
721
- viewport={viewport}
722
- onSelect={handleSelectSection}
723
- onAddSection={() => setAddOpen(true)}
724
- structuralTypes={structuralTypes}
725
- sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
1026
+ <ReflowContext.Provider value={reflowContext}>
1027
+ <div className="prenta-editor-body relative flex flex-1 overflow-hidden">
1028
+ <EditorRail collapsedLabel={activeRailTab.label}>
1029
+ {({ width, dragging, collapse }) => (
1030
+ <>
1031
+ <EditorRailTabs tabs={railTabs} active={railTab} onChange={setRailTab} />
1032
+ <RailHeader
1033
+ title={activeRailTab.title}
1034
+ width={width}
1035
+ dragging={dragging}
1036
+ onCollapse={collapse}
1037
+ />
1038
+
1039
+ <div
1040
+ role="tabpanel"
1041
+ id={`rail-panel-${railTab}`}
1042
+ aria-labelledby={`rail-tab-${railTab}`}
1043
+ className="flex min-h-0 flex-1 flex-col"
1044
+ >
1045
+ {railTab === 'sections' ? (
1046
+ <SectionsList
1047
+ sections={page.sections}
1048
+ selectedId={selectedId}
1049
+ canEdit={effectiveCanEdit}
1050
+ onSelect={handleSelectSection}
1051
+ onToggleVisibility={handleToggleVisibility}
1052
+ />
1053
+ ) : railTab === 'page' ? (
1054
+ <div className="min-h-0 flex-1 overflow-y-auto">
1055
+ <PageTab
1056
+ page={page}
1057
+ canEdit={effectiveCanEdit}
1058
+ publishedPath={publishedPath}
1059
+ siteUrl={publicSiteUrl}
1060
+ directives={directives}
1061
+ onDirectivesChange={(next) => {
1062
+ setDirectives(next)
1063
+ setDirty(true)
1064
+ }}
1065
+ redirectOnPublish={redirectOnPublish}
1066
+ onRedirectOnPublishChange={setRedirectOnPublish}
1067
+ canCreateRedirect={canCreateRedirect}
1068
+ onPageChange={handlePageSettings}
1069
+ onDuplicate={handleDuplicate}
1070
+ onTrash={handleTrash}
1071
+ schemaType={seoRecord?.structuredDataType ?? undefined}
1072
+ brandName={config?.seo?.siteName ?? undefined}
1073
+ />
1074
+ </div>
1075
+ ) : (
1076
+ <SeoEditorPane
1077
+ entityType="page"
1078
+ entityId={page.id}
1079
+ slug={page.slug}
1080
+ auditSections={page.sections}
1081
+ onAuditChange={setLiveAudit}
1082
+ contentHtml={seoContentHtml}
1083
+ draftMeta={{
1084
+ seoTitle: page.seoTitle,
1085
+ seoDescription: page.seoDescription,
1086
+ }}
1087
+ onDraftChange={handlePageSettings}
1088
+ onClose={() => setRailTab('sections')}
1089
+ onSaved={handleSeoSaved}
1090
+ />
1091
+ )}
1092
+ </div>
1093
+
1094
+ {/* Overlays the rail rather than replacing its contents, so
1095
+ closing history returns to exactly the tab and scroll
1096
+ position it was opened from. */}
1097
+ {historyOpen && (
1098
+ <VersionHistoryPanel
1099
+ versions={versions}
1100
+ loading={versionsLoading}
1101
+ error={versionsError}
1102
+ selectedIndex={versionIndex}
1103
+ onSelect={setVersionIndex}
1104
+ onClose={closeHistory}
1105
+ onRestore={handleRestoreVersion}
1106
+ restoring={restoring}
1107
+ canRestore={canRestoreVersion}
1108
+ />
1109
+ )}
1110
+ </>
1111
+ )}
1112
+ </EditorRail>
1113
+
1114
+ <div className="flex-1 overflow-hidden">
1115
+ {sitePreview.active ? (
1116
+ <SitePreviewFrame
1117
+ src={sitePreview.url}
1118
+ viewport={viewport}
1119
+ reloadKey={sitePreview.reloadKey}
1120
+ title={`Site preview: ${page.title || 'Untitled page'}`}
1121
+ selectedId={selectedId}
1122
+ sections={page.sections}
1123
+ suppressSectionsEcho={suppressSectionsEcho}
1124
+ onSelectSection={handleSelectSection}
1125
+ onCanvasEdit={handleCanvasEdit}
1126
+ onCanvasRequestMedia={handleCanvasRequestMedia}
1127
+ />
1128
+ ) : (
1129
+ <EditorCanvas
1130
+ sections={canvasSections}
1131
+ selectedId={selectedId}
1132
+ breakpoint={viewport}
1133
+ zoom={zoom}
1134
+ onScaleChange={setCanvasScale}
1135
+ onCanvasElement={setCanvasEl}
1136
+ onSelect={handleSelectSection}
1137
+ structuralTypes={structuralTypes}
1138
+ sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
1139
+ reviewing={reviewing}
1140
+ changedSectionIds={reviewChangedIds}
1141
+ reviewBar={
1142
+ reviewVersion ? (
1143
+ <ReviewBar
1144
+ version={reviewVersion}
1145
+ changedCount={reviewChangedIds.size}
1146
+ onRestore={() => void handleRestoreVersion(reviewVersion)}
1147
+ onBackToCurrent={() => setVersionIndex(0)}
1148
+ restoring={restoring}
1149
+ canRestore={canRestoreVersion}
1150
+ />
1151
+ ) : null
1152
+ }
1153
+ />
1154
+ )}
1155
+ </div>
1156
+
1157
+ {selected && railTab === 'sections' && !reviewing && (
1158
+ <SectionInspector
1159
+ section={selected}
1160
+ canEdit={effectiveCanEdit}
1161
+ onClose={() => setSelectedId(null)}
1162
+ onContentChange={handleContentChange}
1163
+ onSettingsChange={handleSettingsChange}
1164
+ onMetaChange={handleMetaChange}
1165
+ onToggleVisibility={() => handleToggleVisibility(selected.id)}
726
1166
  />
727
1167
  )}
728
1168
  </div>
729
-
730
- {selected && !seoOpen && (
731
- <SectionInspector
732
- section={selected}
733
- canEdit={effectiveCanEdit}
734
- onClose={() => setSelectedId(null)}
735
- onContentChange={handleContentChange}
736
- onSettingsChange={handleSettingsChange}
737
- onMetaChange={handleMetaChange}
738
- onToggleVisibility={() => handleToggleVisibility(selected.id)}
739
- />
740
- )}
741
-
742
- {seoOpen && (
743
- <EditorSeoAside
744
- entityType="page"
745
- entityId={page.id}
746
- title={page.title}
747
- slug={page.slug}
748
- seoTitle={page.seoTitle}
749
- seoDescription={page.seoDescription}
750
- sections={page.sections}
751
- canEdit={effectiveCanEdit}
752
- onClose={() => setSeoOpen(false)}
753
- onDraftChange={handlePageSettings}
754
- onSaved={handleSeoSaved}
755
- />
756
- )}
757
- </div>
1169
+ </ReflowContext.Provider>
758
1170
  </div>
759
1171
 
760
- <AddSectionModal open={addOpen} onClose={() => setAddOpen(false)} onAdd={handleAdd} />
761
-
762
- {page.id && (
763
- <VersionHistory
764
- collectionSlug="pages"
765
- documentId={page.id}
766
- open={historyOpen}
767
- onClose={() => setHistoryOpen(false)}
768
- onRestore={handleRestored}
769
- />
770
- )}
771
-
772
1172
  <PageSettingsModal
773
1173
  open={settingsOpen}
774
1174
  page={page}
@@ -777,16 +1177,6 @@ export function PageSectionEditor({
777
1177
  onSave={handlePageSettings}
778
1178
  />
779
1179
 
780
- <ConfirmDialog
781
- open={deleteTargetId !== null}
782
- onClose={() => setDeleteTargetId(null)}
783
- onConfirm={confirmDelete}
784
- title="Delete section?"
785
- description="This removes the section from the page. You can undo by leaving without saving."
786
- confirmLabel="Delete"
787
- destructive
788
- />
789
-
790
1180
  <ConfirmDialog
791
1181
  open={confirmPublish}
792
1182
  onClose={() => setConfirmPublish(false)}