@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
@@ -1,32 +1,64 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  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
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 } from '../../components/EditorRailTabs.js';
10
12
  import { ErrorBoundary } from '../../components/ErrorBoundary.js';
11
- import { VersionHistory } from '../../components/VersionHistory.js';
12
- import { addSection, createSection, duplicateSection, emptyPage, fetchPageForEditor, missingPageFields, publishPage, PAGES_COLLECTION, removeSection, reorderSections, savePageDraft, toggleSectionVisibility, updateSectionContent, updateSectionContentAtPath, updateSectionMeta, updateSectionSettings, validatePage, } from '../../lib/page-editor-service.js';
13
+ import { VersionHistoryPanel } from './VersionHistoryPanel.js';
14
+ import { ReviewBar } from './ReviewBar.js';
15
+ import { fetchPageVersions, restorePageVersion, } from '../../lib/version-history.js';
16
+ import { changedSectionIds, diffVersions } from '../../lib/version-diff.js';
17
+ import { emptyPage, fetchPageForEditor, missingPageFields, publishPage, PAGES_COLLECTION, savePageDraft, toggleSectionVisibility, updateSectionContent, updateSectionContentAtPath, updateSectionMeta, updateSectionSettings, validatePage, } from '../../lib/page-editor-service.js';
13
18
  import { MediaPickerModal } from '../../components/MediaPickerModal.js';
14
19
  import { buildPublicPreviewUrl, buildPublicUrl, createPreviewToken, } from '../../lib/preview-link.js';
15
20
  import { usePublicSiteUrl } from '../../hooks/usePublicSiteUrl.js';
16
21
  import { DEFAULT_VIEWPORT } from './viewports.js';
22
+ import { snapshotBaseline } from '../../lib/reflow-baseline.js';
23
+ import { DEFAULT_DIRECTIVES, directivesFromData, } from '@prenta/core/seo/robots-directives';
24
+ import { createPublishRedirect, pendingSlugChange } from '../../lib/page-redirect.js';
25
+ import { updateSeoFields } from '../../lib/seo-service.js';
26
+ import { PageTab } from './PageTab.js';
27
+ import { duplicatePage, deletePage } from '../../lib/pages-service.js';
28
+ import { scoreBand } from '@prenta/core/seo/page-audit';
29
+ import { usePageAudit } from '../../hooks/usePageAudit.js';
17
30
  import { hasCanvasRenderer, useAdminSectionRenderers } from './sections/index.js';
18
31
  import { EditorTopBar } from './EditorTopBar.js';
19
- import { SectionsPanel } from './SectionsPanel.js';
32
+ import { SectionsList } from './SectionsPanel.js';
20
33
  import { EditorCanvas } from './EditorCanvas.js';
34
+ import { ReflowContext } from './reflow-context.js';
21
35
  import { SitePreviewFrame } from './SitePreviewFrame.js';
22
36
  import { useSitePreview } from './useSitePreview.js';
23
37
  import { SectionInspector } from './SectionInspector.js';
24
- import { AddSectionModal } from './AddSectionModal.js';
25
38
  import { PageSettingsModal } from './PageSettingsModal.js';
26
39
  import { ValidationSummary } from './ValidationSummary.js';
27
40
  import { resolveDefaultSitePreview } from '../../lib/editor-preview.js';
28
- import { computeLiveSeoScore } from '../../lib/editor-seo-content.js';
41
+ import { buildEditorContentHtml } from '../../lib/editor-seo-content.js';
29
42
  import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js';
43
+ /**
44
+ * The lower of the two audit scores — the tab dot should reflect the half that
45
+ * needs work, not an average that hides it.
46
+ *
47
+ * Three-valued on purpose, and it must NOT fall back to another scorer:
48
+ * undefined — the audit has not reported yet (the SEO tab has never been
49
+ * opened). No dot beats a dot from a different model.
50
+ * null — both groups are gated off, so there is genuinely nothing to
51
+ * rate. An earlier version fell back to `computeLiveSeoScore`
52
+ * here, which put a number from the content analyser — different
53
+ * model, different bands — on a tab summarising the audit. That
54
+ * is the disagreement this wiring exists to remove.
55
+ */
56
+ function weakestAuditScore(audit) {
57
+ if (!audit)
58
+ return undefined;
59
+ const scores = [audit.seo.score, audit.geo.score].filter((s) => s !== null);
60
+ return scores.length ? Math.min(...scores) : null;
61
+ }
30
62
  function deriveCan(session) {
31
63
  const role = session?.user?.role;
32
64
  // Default closed: when we can't identify a role, hide the edit/publish
@@ -34,10 +66,18 @@ function deriveCan(session) {
34
66
  // access on every write, so this is a UX guard — but it should fail safe.
35
67
  // (Matches the Post editor's behavior.)
36
68
  if (!role)
37
- return { canEdit: false, canPublish: false };
69
+ return { canEdit: false, canPublish: false, canCreateRedirect: false };
38
70
  const editors = ['ADMIN', 'EDITOR', 'AUTHOR'];
39
71
  const publishers = ['ADMIN', 'EDITOR'];
40
- return { canEdit: editors.includes(role), canPublish: publishers.includes(role) };
72
+ // Same roles that may publish a page may create the 301 that follows a
73
+ // slug move. Offering the checkbox to anyone else promises a redirect
74
+ // `POST /redirects` will reject.
75
+ const redirectCreators = ['OWNER', 'ADMIN', 'EDITOR'];
76
+ return {
77
+ canEdit: editors.includes(role),
78
+ canPublish: publishers.includes(role),
79
+ canCreateRedirect: redirectCreators.includes(role),
80
+ };
41
81
  }
42
82
  export function PageSectionEditor({ documentId, initialParentId, config, session, onNavigate, onOpenPreview, }) {
43
83
  const [page, setPage] = useState(null);
@@ -51,19 +91,83 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
51
91
  /** Canvas image Replace — opens MediaPickerModal for this field path. */
52
92
  const [canvasMediaTarget, setCanvasMediaTarget] = useState(null);
53
93
  const [viewport, setViewport] = useState(DEFAULT_VIEWPORT);
94
+ const [zoom, setZoom] = useState('fit');
95
+ /**
96
+ * Live audit reported by the SEO pane, built from unsaved form state. Null
97
+ * until that pane has mounted — `audit` below falls back to the saved-record
98
+ * audit so the header chip and tab dot always have a number.
99
+ */
100
+ const [liveAudit, setLiveAudit] = useState(null);
101
+ /** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
102
+ const [canvasScale, setCanvasScale] = useState(1);
103
+ /**
104
+ * The live canvas surface, for the reflow guard to clone. State rather than a
105
+ * ref because the context value has to update when it mounts — the canvas
106
+ * unmounts whenever the site-preview frame takes over.
107
+ */
108
+ const [canvasEl, setCanvasEl] = useState(null);
109
+ /**
110
+ * Section content as it is on the live site. Captured whenever the editor
111
+ * takes delivery of a PUBLISHED page — on load, and after each save and
112
+ * publish, both of which write straight through to the live content.
113
+ */
114
+ const [baseline, setBaseline] = useState(null);
115
+ /**
116
+ * Path as it is on the live site, captured with the reflow baseline and for
117
+ * the same reason: a slug change only breaks links if something is already
118
+ * live at the old address.
119
+ */
120
+ const [publishedPath, setPublishedPath] = useState(null);
121
+ const [directives, setDirectives] = useState(DEFAULT_DIRECTIVES);
122
+ /**
123
+ * Defaults ON. The editor changing a live page's slug almost always wants the
124
+ * redirect, and the cost of an unwanted one is a rule they can delete, while
125
+ * the cost of a missing one is 404s discovered by visitors.
126
+ */
127
+ const [redirectOnPublish, setRedirectOnPublish] = useState(true);
128
+ // The reflow guard's inputs, gathered in one place. Memoised because a new
129
+ // object identity on every render would restart the debounce in every guarded
130
+ // field's effect — the measurement would then never settle while typing,
131
+ // which is precisely when it is wanted.
132
+ const reflowContext = useMemo(() => ({
133
+ canvas: canvasEl,
134
+ sectionId: selectedId,
135
+ baseline,
136
+ device: viewport,
137
+ onProof: setViewport,
138
+ }), [canvasEl, selectedId, baseline, viewport]);
139
+ /**
140
+ * Take delivery of server state.
141
+ *
142
+ * Every path that receives a fresh page from the API goes through here so the
143
+ * reflow baseline is re-captured with it. Saving a PUBLISHED page writes
144
+ * straight through to the live content — there is no draft/published split —
145
+ * so a save moves the baseline exactly as a publish does. Missing one would
146
+ * leave the guard comparing against content that is no longer on the site.
147
+ */
148
+ const acceptPage = useCallback((p) => {
149
+ setPage(p);
150
+ setBaseline(snapshotBaseline(p.sections, p.status));
151
+ setPublishedPath(p.status === 'PUBLISHED' ? p.path : null);
152
+ }, []);
54
153
  const [dirty, setDirty] = useState(false);
55
154
  const [saveState, setSaveState] = useState('idle');
56
155
  const [publishing, setPublishing] = useState(false);
57
- const [addOpen, setAddOpen] = useState(false);
58
156
  const [settingsOpen, setSettingsOpen] = useState(false);
59
- const [seoOpen, setSeoOpen] = useState(false);
157
+ /** Active rail tab. Replaces the old boolean `seoOpen` right-hand aside. */
158
+ const [railTab, setRailTab] = useState('sections');
60
159
  const [historyOpen, setHistoryOpen] = useState(false);
61
- const [deleteTargetId, setDeleteTargetId] = useState(null);
160
+ const [versions, setVersions] = useState([]);
161
+ const [versionsLoading, setVersionsLoading] = useState(false);
162
+ const [versionsError, setVersionsError] = useState(null);
163
+ /** Index into `versions`; 0 is the newest, which is the current content. */
164
+ const [versionIndex, setVersionIndex] = useState(0);
165
+ const [restoring, setRestoring] = useState(false);
62
166
  const [confirmPublish, setConfirmPublish] = useState(false);
63
167
  const [scheduleOpen, setScheduleOpen] = useState(false);
64
168
  const [leaveTarget, setLeaveTarget] = useState(null);
65
169
  const [validation, setValidation] = useState(null);
66
- const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session]);
170
+ const { canEdit, canPublish, canCreateRedirect } = useMemo(() => deriveCan(session), [session]);
67
171
  const customRenderers = useAdminSectionRenderers();
68
172
  const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl);
69
173
  // A misconfigured `pages` collection (missing the fields the editor
@@ -81,18 +185,152 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
81
185
  }, [config]);
82
186
  const publishCheck = useMemo(() => (page ? validatePage(page, true) : { errors: [''], warnings: [] }), [page]);
83
187
  const publishReady = Boolean(page) && publishCheck.errors.length === 0;
84
- const liveSeoScore = useMemo(() => {
85
- if (!page)
86
- return 0;
87
- return computeLiveSeoScore({
88
- title: page.title,
89
- slug: page.slug,
90
- seoTitle: page.seoTitle,
91
- seoDescription: page.seoDescription,
92
- sections: page.sections,
93
- });
94
- }, [page]);
95
188
  const publishDisabledReason = publishCheck.errors[0];
189
+ // Saved-record audit, available from first paint. Both this and the pane's
190
+ // live audit go through `buildPageAuditInput`, so they can differ only in
191
+ // saved-versus-unsaved metadata — never in how a check is decided.
192
+ const { audit: savedAudit, record: seoRecord, reload: reloadAudit, } = usePageAudit({
193
+ entityType: 'page',
194
+ entityId: page?.id ?? null,
195
+ sections: page?.sections ?? [],
196
+ slug: page?.slug ?? '',
197
+ fallback: { title: page?.seoTitle, description: page?.seoDescription },
198
+ });
199
+ // Seed directives from the saved record. Keyed on the record's identity, not
200
+ // on its fields — re-running on every field change would discard whatever the
201
+ // editor had just toggled and not yet saved.
202
+ useEffect(() => {
203
+ if (!seoRecord)
204
+ return;
205
+ setDirectives(directivesFromData({
206
+ noindex: seoRecord.noindex,
207
+ nofollow: seoRecord.nofollow,
208
+ nosnippet: seoRecord.nosnippet,
209
+ noarchive: seoRecord.noarchive,
210
+ maxImagePreview: seoRecord.maxImagePreview,
211
+ }));
212
+ }, [seoRecord?.id]);
213
+ // Unsaved edits win: while the SEO pane is open its number is the truthful
214
+ // one, and every surface reads this single value.
215
+ const audit = liveAudit ?? savedAudit;
216
+ // Content the SEO pane analyses. Hoisted out of the old aside wrapper so the
217
+ // rail can render the pane directly — the aside only ever existed to supply
218
+ // a resizable container, which the rail now is.
219
+ const seoContentHtml = useMemo(() => buildEditorContentHtml(page?.sections ?? []), [page?.sections]);
220
+ const railTabs = useMemo(() => [
221
+ {
222
+ id: 'sections',
223
+ label: 'Sections',
224
+ title: 'Page content',
225
+ count: page?.sections.length ?? 0,
226
+ },
227
+ {
228
+ id: 'seo',
229
+ label: 'SEO & GEO',
230
+ title: 'Search & AI readiness',
231
+ // The weaker of the two: a page can be perfectly crawlable and still
232
+ // invisible to answer engines, and a dot that averaged them would hide
233
+ // whichever half is failing. `null` means a gate is off, which the tab
234
+ // reports as unrated rather than as a low score.
235
+ score: weakestAuditScore(audit),
236
+ },
237
+ {
238
+ id: 'page',
239
+ label: 'Page',
240
+ title: 'Page settings',
241
+ },
242
+ ], [page?.sections.length, audit]);
243
+ const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0];
244
+ // ─── Version history ─────────────────────────────────────────────────
245
+ // History is a MODE, not a dialog: it overlays the rail and drives the
246
+ // canvas, so the version being reviewed is editor state rather than
247
+ // something owned by a panel that can be unmounted.
248
+ const loadVersions = useCallback(async (pageId) => {
249
+ setVersionsLoading(true);
250
+ setVersionsError(null);
251
+ try {
252
+ setVersions(await fetchPageVersions(PAGES_COLLECTION, pageId));
253
+ }
254
+ catch (err) {
255
+ setVersions([]);
256
+ setVersionsError(err instanceof Error ? err.message : 'Failed to load version history');
257
+ }
258
+ finally {
259
+ setVersionsLoading(false);
260
+ }
261
+ }, []);
262
+ const openHistory = useCallback(() => {
263
+ if (!page?.id)
264
+ return;
265
+ setHistoryOpen(true);
266
+ // Always land on "current" so opening history never silently swaps the
267
+ // canvas out from under the editor.
268
+ setVersionIndex(0);
269
+ void loadVersions(page.id);
270
+ }, [page?.id, loadVersions]);
271
+ const closeHistory = useCallback(() => {
272
+ setHistoryOpen(false);
273
+ setVersionIndex(0);
274
+ }, []);
275
+ /**
276
+ * Index 0 is the current content, which the canvas is already showing — so
277
+ * selecting it is "back to current", not a review. Only an older index puts
278
+ * the editor into review mode.
279
+ */
280
+ const reviewing = historyOpen && versionIndex > 0;
281
+ const reviewVersion = reviewing ? (versions[versionIndex] ?? null) : null;
282
+ // What that version's save CHANGED — diffed against the one before it, which
283
+ // is the question the outlines on the canvas answer.
284
+ const reviewChanges = useMemo(() => reviewVersion
285
+ ? diffVersions(reviewVersion.sections, versions[versionIndex + 1]?.sections)
286
+ : [], [reviewVersion, versions, versionIndex]);
287
+ const reviewChangedIds = useMemo(() => changedSectionIds(reviewChanges), [reviewChanges]);
288
+ const handleRestoreVersion = useCallback(async (version) => {
289
+ if (!page?.id)
290
+ return;
291
+ setRestoring(true);
292
+ try {
293
+ await restorePageVersion(PAGES_COLLECTION, page.id, version.id);
294
+ // Re-read rather than trusting the snapshot we already hold: the
295
+ // restore goes through the normal update path, so hooks and derived
296
+ // fields may have altered what actually landed.
297
+ const restored = await fetchPageForEditor(page.id);
298
+ acceptPage(restored);
299
+ setSelectedId(null);
300
+ setDirty(false);
301
+ setSaveState('idle');
302
+ setValidation(null);
303
+ closeHistory();
304
+ void loadVersions(page.id);
305
+ toast.success('Version restored');
306
+ }
307
+ catch (err) {
308
+ toast.error(err instanceof Error ? err.message : 'Failed to restore version');
309
+ }
310
+ finally {
311
+ setRestoring(false);
312
+ }
313
+ }, [page?.id, acceptPage, closeHistory, loadVersions]);
314
+ // ─── Edit permission ─────────────────────────────────────────────────
315
+ /**
316
+ * ADR 0002: a Page-Builder-owned page (it has a `_layout`) compiles its
317
+ * sections from the builder layout on every save, so editing them here would
318
+ * be overwritten. The server independently refuses such edits with 409.
319
+ */
320
+ const readOnly = page?.builderOwned ?? false;
321
+ /**
322
+ * The single gate every writing surface reads.
323
+ *
324
+ * Hoisted above the callbacks rather than derived at render time, because
325
+ * the two that were left behind were the ones that mattered: the site
326
+ * preview's in-place edit handlers close over `canEdit` (the raw role) and
327
+ * so kept accepting edits while a past version was under review — and while
328
+ * a builder-owned page was supposedly read-only. The iframe is a second
329
+ * editing surface, and a lock that only covers the rail is not a lock.
330
+ */
331
+ const effectiveCanEdit = canEdit && !readOnly && !reviewing;
332
+ /** Restoring is not editing: it is allowed precisely while reviewing. */
333
+ const canRestoreVersion = canEdit && !readOnly;
96
334
  // ─── Load ──────────────────────────────────────────────────────────
97
335
  useEffect(() => {
98
336
  let cancelled = false;
@@ -106,7 +344,7 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
106
344
  return;
107
345
  }
108
346
  if (!documentId) {
109
- setPage(emptyPage(initialParentId ?? null));
347
+ acceptPage(emptyPage(initialParentId ?? null));
110
348
  setLoading(false);
111
349
  // Start a brand-new page the way WordPress does: name it first. The
112
350
  // settings modal opens with the title focused and the slug/path
@@ -118,7 +356,7 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
118
356
  .then((p) => {
119
357
  if (cancelled)
120
358
  return;
121
- setPage(p);
359
+ acceptPage(p);
122
360
  // Default to a clean canvas (no selection / no inspector) to match
123
361
  // the editor's default composition; the inspector opens only once
124
362
  // the user picks a section to edit.
@@ -144,22 +382,7 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
144
382
  setDirty(true);
145
383
  setSaveState('idle');
146
384
  }, []);
147
- const handleAdd = useCallback((type) => {
148
- const section = createSection(type);
149
- mutateSections((s) => addSection(s, section));
150
- setSelectedId(section.id);
151
- setAddOpen(false);
152
- }, [mutateSections]);
153
- const handleReorder = useCallback((orderedIds) => mutateSections((s) => reorderSections(s, orderedIds)), [mutateSections]);
154
385
  const handleToggleVisibility = useCallback((id) => mutateSections((s) => toggleSectionVisibility(s, id)), [mutateSections]);
155
- const handleDuplicate = useCallback((id) => mutateSections((s) => duplicateSection(s, id)), [mutateSections]);
156
- const confirmDelete = useCallback(() => {
157
- if (!deleteTargetId)
158
- return;
159
- mutateSections((s) => removeSection(s, deleteTargetId));
160
- setSelectedId((cur) => (cur === deleteTargetId ? null : cur));
161
- setDeleteTargetId(null);
162
- }, [deleteTargetId, mutateSections]);
163
386
  const handleContentChange = useCallback((patch) => {
164
387
  if (!selectedId)
165
388
  return;
@@ -182,6 +405,31 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
182
405
  setSaveState('idle');
183
406
  }, []);
184
407
  // ─── Save / publish ──────────────────────────────────────────────────
408
+ /**
409
+ * Persist robots directives. They live on the SEO record, not the document
410
+ * body, so they need their own write — and it has to happen on both save and
411
+ * publish, or a toggle would appear to stick until the next reload.
412
+ *
413
+ * Reported on failure rather than swallowed: a directive that silently failed
414
+ * to save is the same defect as one that never reached the head.
415
+ */
416
+ const saveDirectives = useCallback(async (pageId) => {
417
+ const { error } = await updateSeoFields('page', pageId, {
418
+ noindex: !directives.index,
419
+ nofollow: !directives.follow,
420
+ nosnippet: !directives.snippet,
421
+ noarchive: !directives.archive,
422
+ maxImagePreview: directives.maxImagePreview,
423
+ });
424
+ if (error) {
425
+ toast.error(`Search directives were not saved: ${error}`);
426
+ return false;
427
+ }
428
+ // The header chip and rail dot read the saved SEO record. Without a
429
+ // reload they keep the pre-save score after noindex/nosnippet flips.
430
+ await reloadAudit();
431
+ return true;
432
+ }, [directives, reloadAudit]);
185
433
  const handleSaveDraft = useCallback(async () => {
186
434
  if (!page)
187
435
  return false;
@@ -195,7 +443,9 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
195
443
  setSaveState('saving');
196
444
  try {
197
445
  const saved = await savePageDraft(page);
198
- setPage(saved);
446
+ if (saved.id)
447
+ await saveDirectives(saved.id);
448
+ acceptPage(saved);
199
449
  setDirty(false);
200
450
  setSaveState('saved');
201
451
  setValidation(null);
@@ -209,7 +459,7 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
209
459
  toast.error(err instanceof Error ? err.message : 'Failed to save draft');
210
460
  return false;
211
461
  }
212
- }, [page, documentId, onNavigate]);
462
+ }, [page, documentId, onNavigate, saveDirectives, acceptPage]);
213
463
  // Inline branded preview — Next builder canvas (same origin as admin), not
214
464
  // the Astro public apex. "View on site" still uses publicSiteUrl below.
215
465
  const sitePreview = useSitePreview({
@@ -271,8 +521,25 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
271
521
  return;
272
522
  setPublishing(true);
273
523
  try {
524
+ // The move has to be read BEFORE publishing — `acceptPage` moves the
525
+ // published path to the new one, after which there is nothing to detect.
526
+ const move = redirectOnPublish && canCreateRedirect ? pendingSlugChange(publishedPath, page.path) : null;
274
527
  const published = await publishPage(page);
275
- setPage(published);
528
+ if (published.id)
529
+ await saveDirectives(published.id);
530
+ if (move) {
531
+ const { error } = await createPublishRedirect(move);
532
+ if (error) {
533
+ // Loud, and naming both paths: the page has already moved, so a
534
+ // silently-failed redirect leaves exactly the broken links the switch
535
+ // promised to prevent.
536
+ toast.error(`Published, but the redirect ${move.from} → ${move.to} failed: ${error}`);
537
+ }
538
+ else {
539
+ toast.success(`Redirect created: ${move.from} → ${move.to}`);
540
+ }
541
+ }
542
+ acceptPage(published);
276
543
  setDirty(false);
277
544
  setSaveState('saved');
278
545
  setValidation(null);
@@ -286,59 +553,84 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
286
553
  finally {
287
554
  setPublishing(false);
288
555
  }
289
- }, [page, documentId, onNavigate]);
290
- // After a version restore the server has already written the restored
291
- // data — refetch so the editor reflects it instead of the stale state.
292
- const handleRestored = useCallback(() => {
556
+ }, [
557
+ page,
558
+ documentId,
559
+ onNavigate,
560
+ saveDirectives,
561
+ redirectOnPublish,
562
+ canCreateRedirect,
563
+ publishedPath,
564
+ acceptPage,
565
+ ]);
566
+ const handleDuplicate = useCallback(async () => {
293
567
  if (!page?.id)
294
568
  return;
295
- fetchPageForEditor(page.id)
296
- .then((p) => {
297
- setPage(p);
298
- setSelectedId(null);
299
- setSeoOpen(false);
300
- setDirty(false);
301
- setSaveState('idle');
302
- setValidation(null);
303
- })
304
- .catch(() => {
305
- toast.error('Version restored — refresh the page to see it.');
306
- });
307
- }, [page?.id]);
569
+ const { id, error } = await duplicatePage(page.id);
570
+ if (error || !id) {
571
+ toast.error(error ?? 'Could not duplicate the page');
572
+ return;
573
+ }
574
+ toast.success('Page duplicated');
575
+ // Navigate to the copy rather than staying put. The duplicate is a draft
576
+ // named "(Copy)", and leaving the editor on the original makes it look as
577
+ // though nothing happened.
578
+ onNavigate(`/pages/${id}/edit`);
579
+ }, [page?.id, onNavigate]);
580
+ const handleTrash = useCallback(async () => {
581
+ if (!page?.id)
582
+ return;
583
+ const { error } = await deletePage(page.id);
584
+ if (error) {
585
+ toast.error(error);
586
+ return;
587
+ }
588
+ toast.success('Page moved to trash');
589
+ onNavigate('/pages');
590
+ }, [page?.id, onNavigate]);
308
591
  const handleSeoSaved = useCallback(() => {
592
+ reloadAudit();
309
593
  if (!page?.id)
310
594
  return;
311
595
  fetchPageForEditor(page.id)
312
- .then((p) => setPage(p))
596
+ .then((p) => acceptPage(p))
313
597
  .catch(() => {
314
598
  /* SEO API saved; editor meta will sync on next full load */
315
599
  });
316
- }, [page?.id]);
600
+ }, [page?.id, reloadAudit]);
601
+ // The header's SEO chip is a shortcut to the rail's SEO tab. Toggling back
602
+ // returns to Sections rather than leaving the rail on a tab the chip no
603
+ // longer appears to be showing.
317
604
  const handleToggleSeo = useCallback(() => {
318
- setSeoOpen((open) => {
319
- if (!open)
605
+ setRailTab((tab) => {
606
+ if (tab !== 'seo')
320
607
  setSelectedId(null);
321
- return !open;
608
+ return tab === 'seo' ? 'sections' : 'seo';
322
609
  });
323
610
  }, []);
324
611
  const handleSelectSection = useCallback((id, fieldPath) => {
325
- setSeoOpen(false);
612
+ // Selecting on the canvas is a request to edit that section — show the
613
+ // outline it belongs to, whichever tab was open.
614
+ setRailTab('sections');
326
615
  setSelectedId(id);
327
616
  setPendingFocusPath(fieldPath ?? null);
328
617
  }, []);
618
+ // `effectiveCanEdit`, not `canEdit`: the site preview is a second editing
619
+ // surface, so a role check alone let it write through both the version-review
620
+ // lock and the builder-owned lock the rest of the editor honours.
329
621
  const handleCanvasEdit = useCallback((edit) => {
330
- if (!canEdit)
622
+ if (!effectiveCanEdit)
331
623
  return;
332
624
  setSuppressSectionsEcho((n) => n + 1);
333
625
  mutateSections((s) => updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value));
334
626
  setSelectedId(edit.sectionId);
335
- }, [canEdit, mutateSections]);
627
+ }, [effectiveCanEdit, mutateSections]);
336
628
  const handleCanvasRequestMedia = useCallback((request) => {
337
- if (!canEdit)
629
+ if (!effectiveCanEdit)
338
630
  return;
339
631
  setSelectedId(request.sectionId);
340
632
  setCanvasMediaTarget(request);
341
- }, [canEdit]);
633
+ }, [effectiveCanEdit]);
342
634
  // After a canvas field click, focus the matching inspector input once painted.
343
635
  useEffect(() => {
344
636
  if (!pendingFocusPath || !selectedId)
@@ -439,12 +731,11 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
439
731
  return (_jsx("div", { className: "bg-background flex h-full items-center justify-center p-6", children: _jsxs("div", { className: "border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4", children: [_jsx(AlertTriangle, { className: "text-destructive shrink-0", size: 20, "aria-hidden": true }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: "Failed to load page" }), _jsx("p", { className: "text-muted-foreground mt-1 text-sm", children: loadError ?? 'Page not found.' }), _jsx("button", { type: "button", onClick: () => onNavigate('/pages'), className: "text-primary mt-2 text-sm font-medium hover:underline", children: "Back to Pages" })] })] }) }));
440
732
  }
441
733
  const selected = page.sections.find((s) => s.id === selectedId) ?? null;
442
- // ADR 0002: a Page-Builder-owned page (it has a `_layout`) compiles its
443
- // sections from the builder layout on every save, so editing them here would
444
- // be overwritten. Open this editor read-only and route the user to the
445
- // builder. The server independently refuses such edits with 409.
446
- const readOnly = page.builderOwned;
447
- const effectiveCanEdit = canEdit && !readOnly;
734
+ // `readOnly` / `effectiveCanEdit` / `canRestoreVersion` are computed above,
735
+ // with the hooks, so the callbacks defined there share this one gate.
736
+ // The canvas shows the reviewed snapshot, but `page.sections` stays the
737
+ // editor's own content throughout nothing about review mode writes to it.
738
+ const canvasSections = reviewVersion ? reviewVersion.sections : page.sections;
448
739
  // Sections whose type has no registered definition (seed/code-owned, e.g.
449
740
  // `opal-*`). They are preserved verbatim on save but can't be edited here.
450
741
  const unmanagedTypes = Array.from(new Set(page.sections.filter((s) => s.unmanaged).map((s) => s.sectionType)));
@@ -455,7 +746,21 @@ export function PageSectionEditor({ documentId, initialParentId, config, session
455
746
  const structuralTypes = Array.from(new Set(page.sections
456
747
  .filter((s) => !hasCanvasRenderer(s.sectionType) && !customRenderers[s.sectionType])
457
748
  .map((s) => s.sectionType)));
458
- return (_jsxs(ErrorBoundary, { children: [_jsxs("div", { className: "bg-background flex h-full flex-col overflow-hidden", children: [_jsx(EditorTopBar, { title: page.title, status: page.status, dirty: dirty, saveState: saveState, publishing: publishing, canEdit: effectiveCanEdit, canPublish: canPublish && effectiveCanEdit, publishReady: publishReady, publishDisabledReason: publishDisabledReason, viewport: viewport, onViewport: setViewport, onBack: () => guardedNavigate('/pages'), onPreview: handlePreview, onViewOnSite: page.id ? handleViewOnSite : undefined, sitePreview: sitePreview.active, onToggleSitePreview: page.id && page.slug ? sitePreview.toggle : undefined, onHistory: page.id ? () => setHistoryOpen(true) : undefined, onOpenSettings: () => setSettingsOpen(true), onOpenSeo: handleToggleSeo, seoOpen: seoOpen, seoScore: liveSeoScore, onSaveDraft: handleSaveDraft, onPublish: requestPublish, onSchedule: page.id ? requestSchedule : undefined, scheduleReady: Boolean(page.id), scheduledAt: page.scheduledAt, scheduledUnpublishAt: page.scheduledUnpublishAt, structuralTypes: structuralTypes }), readOnly && (_jsxs("div", { role: "status", className: "border-border bg-accent/40 flex items-center gap-3 border-b px-4 py-2.5", children: [_jsx(Blocks, { className: "text-muted-foreground shrink-0", size: 16, "aria-hidden": true }), _jsxs("p", { className: "text-muted-foreground flex-1 text-sm", children: ["This page is built in the", ' ', _jsx("span", { className: "text-foreground font-medium", children: "Page Builder" }), ". Its sections are generated from the builder layout and are read-only here."] }), page.id && (_jsx("button", { type: "button", onClick: () => guardedNavigate(`/page-builder/${page.id}`), className: "bg-primary text-primary-foreground hover:bg-primary/90 shrink-0 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors", children: "Open in Page Builder" }))] })), unmanagedTypes.length > 0 && (_jsxs("div", { role: "status", className: "border-border bg-muted/50 flex items-start gap-3 border-b px-4 py-2.5", children: [_jsx(Lock, { className: "text-muted-foreground mt-0.5 shrink-0", size: 16, "aria-hidden": true }), _jsxs("p", { className: "text-muted-foreground flex-1 text-sm", children: ["This page has", ' ', _jsxs("span", { className: "text-foreground font-medium", children: [unmanagedTypes.length, " section type", unmanagedTypes.length > 1 ? 's' : ''] }), ' ', "managed outside the editor (", unmanagedTypes.join(', '), "). They are preserved on save but edited in your seed script or code, not here."] })] })), validation && (_jsx(ValidationSummary, { errors: validation.errors, warnings: validation.warnings, onDismiss: () => setValidation(null) })), _jsxs("div", { className: "relative flex flex-1 overflow-hidden", children: [_jsx(SectionsPanel, { sections: page.sections, selectedId: selectedId, canEdit: effectiveCanEdit, onSelect: handleSelectSection, onReorder: handleReorder, onToggleVisibility: handleToggleVisibility, onDuplicate: handleDuplicate, onRequestDelete: (id) => setDeleteTargetId(id), onAdd: () => setAddOpen(true) }), _jsx("div", { className: "flex-1 overflow-hidden", children: sitePreview.active ? (_jsx(SitePreviewFrame, { src: sitePreview.url, viewport: viewport, reloadKey: sitePreview.reloadKey, title: `Site preview: ${page.title || 'Untitled page'}`, selectedId: selectedId, sections: page.sections, suppressSectionsEcho: suppressSectionsEcho, onSelectSection: handleSelectSection, onCanvasEdit: handleCanvasEdit, onCanvasRequestMedia: handleCanvasRequestMedia })) : (_jsx(EditorCanvas, { sections: page.sections, selectedId: selectedId, viewport: viewport, onSelect: handleSelectSection, onAddSection: () => setAddOpen(true), structuralTypes: structuralTypes, sitePreviewAvailable: sitePreviewAvailable && !sitePreview.active })) }), selected && !seoOpen && (_jsx(SectionInspector, { section: selected, canEdit: effectiveCanEdit, onClose: () => setSelectedId(null), onContentChange: handleContentChange, onSettingsChange: handleSettingsChange, onMetaChange: handleMetaChange, onToggleVisibility: () => handleToggleVisibility(selected.id) })), seoOpen && (_jsx(EditorSeoAside, { entityType: "page", entityId: page.id, title: page.title, slug: page.slug, seoTitle: page.seoTitle, seoDescription: page.seoDescription, sections: page.sections, canEdit: effectiveCanEdit, onClose: () => setSeoOpen(false), onDraftChange: handlePageSettings, onSaved: handleSeoSaved }))] })] }), _jsx(AddSectionModal, { open: addOpen, onClose: () => setAddOpen(false), onAdd: handleAdd }), page.id && (_jsx(VersionHistory, { collectionSlug: "pages", documentId: page.id, open: historyOpen, onClose: () => setHistoryOpen(false), onRestore: handleRestored })), _jsx(PageSettingsModal, { open: settingsOpen, page: page, canEdit: effectiveCanEdit, onClose: () => setSettingsOpen(false), onSave: handlePageSettings }), _jsx(ConfirmDialog, { open: deleteTargetId !== null, onClose: () => setDeleteTargetId(null), onConfirm: confirmDelete, title: "Delete section?", description: "This removes the section from the page. You can undo by leaving without saving.", confirmLabel: "Delete", destructive: true }), _jsx(ConfirmDialog, { open: confirmPublish, onClose: () => setConfirmPublish(false), onConfirm: doPublish, title: "Publish this page?", description: "The current content will go live and replace the published version.", confirmLabel: "Publish" }), page.id && (_jsx(SchedulePublishDialog, { collectionSlug: PAGES_COLLECTION, documentId: page.id, scheduledAt: page.scheduledAt, scheduledUnpublishAt: page.scheduledUnpublishAt, open: scheduleOpen, onClose: () => setScheduleOpen(false), onScheduled: handleScheduled })), _jsx(ConfirmDialog, { open: leaveTarget !== null, onClose: () => setLeaveTarget(null), onConfirm: () => {
749
+ return (_jsxs(ErrorBoundary, { children: [_jsxs("div", { className: "bg-background flex h-full flex-col overflow-hidden", children: [_jsx(EditorTopBar, { title: page.title, status: page.status, dirty: dirty, saveState: saveState, publishing: publishing, canEdit: effectiveCanEdit, canPublish: canPublish && effectiveCanEdit, publishReady: publishReady, publishDisabledReason: publishDisabledReason, viewport: viewport, onViewport: setViewport, ...(sitePreview.active
750
+ ? // The site preview is an iframe of the real front end, not a
751
+ // scaled canvas — a zoom percentage there would be a lie.
752
+ {}
753
+ : { zoom, canvasScale, onZoom: setZoom }), onBack: () => guardedNavigate('/pages'), onPreview: handlePreview, onViewOnSite: page.id ? handleViewOnSite : undefined, sitePreview: sitePreview.active, onToggleSitePreview: page.id && page.slug ? sitePreview.toggle : undefined, historyOpen: historyOpen, onHistory: page.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined, onOpenSettings: () => setSettingsOpen(true), onOpenSeo: handleToggleSeo, seoOpen: railTab === 'seo',
754
+ // The audit's SEO score, banded by the audit's own scale. Previously
755
+ // this chip showed `computeLiveSeoScore` — a different model — so the
756
+ // header read 53 while the rings beside it read 0 and 57.
757
+ seoScore: audit?.seo.score ?? undefined, seoScoreBand: typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined, onSaveDraft: handleSaveDraft, onPublish: requestPublish, onSchedule: page.id ? requestSchedule : undefined, scheduleReady: Boolean(page.id), scheduledAt: page.scheduledAt, scheduledUnpublishAt: page.scheduledUnpublishAt, structuralTypes: structuralTypes }), readOnly && (_jsxs("div", { role: "status", className: "border-border bg-accent/40 flex items-center gap-3 border-b px-4 py-2.5", children: [_jsx(Blocks, { className: "text-muted-foreground shrink-0", size: 16, "aria-hidden": true }), _jsxs("p", { className: "text-muted-foreground flex-1 text-sm", children: ["This page is built in the", ' ', _jsx("span", { className: "text-foreground font-medium", children: "Page Builder" }), ". Its sections are generated from the builder layout and are read-only here."] }), page.id && (_jsx("button", { type: "button", onClick: () => guardedNavigate(`/page-builder/${page.id}`), className: "bg-primary text-primary-foreground hover:bg-primary/90 shrink-0 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors", children: "Open in Page Builder" }))] })), unmanagedTypes.length > 0 && (_jsxs("div", { role: "status", className: "border-border bg-muted/50 flex items-start gap-3 border-b px-4 py-2.5", children: [_jsx(Lock, { className: "text-muted-foreground mt-0.5 shrink-0", size: 16, "aria-hidden": true }), _jsxs("p", { className: "text-muted-foreground flex-1 text-sm", children: ["This page has", ' ', _jsxs("span", { className: "text-foreground font-medium", children: [unmanagedTypes.length, " section type", unmanagedTypes.length > 1 ? 's' : ''] }), ' ', "managed outside the editor (", unmanagedTypes.join(', '), "). They are preserved on save but edited in your seed script or code, not here."] })] })), validation && (_jsx(ValidationSummary, { errors: validation.errors, warnings: validation.warnings, onDismiss: () => setValidation(null) })), _jsx(ReflowContext.Provider, { value: reflowContext, children: _jsxs("div", { className: "prenta-editor-body relative flex flex-1 overflow-hidden", children: [_jsx(EditorRail, { collapsedLabel: activeRailTab.label, children: ({ width, dragging, collapse }) => (_jsxs(_Fragment, { children: [_jsx(EditorRailTabs, { tabs: railTabs, active: railTab, onChange: setRailTab }), _jsx(RailHeader, { title: activeRailTab.title, width: width, dragging: dragging, onCollapse: collapse }), _jsx("div", { role: "tabpanel", id: `rail-panel-${railTab}`, "aria-labelledby": `rail-tab-${railTab}`, className: "flex min-h-0 flex-1 flex-col", children: railTab === 'sections' ? (_jsx(SectionsList, { sections: page.sections, selectedId: selectedId, canEdit: effectiveCanEdit, onSelect: handleSelectSection, onToggleVisibility: handleToggleVisibility })) : railTab === 'page' ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(PageTab, { page: page, canEdit: effectiveCanEdit, publishedPath: publishedPath, siteUrl: publicSiteUrl, directives: directives, onDirectivesChange: (next) => {
758
+ setDirectives(next);
759
+ setDirty(true);
760
+ }, redirectOnPublish: redirectOnPublish, onRedirectOnPublishChange: setRedirectOnPublish, canCreateRedirect: canCreateRedirect, onPageChange: handlePageSettings, onDuplicate: handleDuplicate, onTrash: handleTrash, schemaType: seoRecord?.structuredDataType ?? undefined, brandName: config?.seo?.siteName ?? undefined }) })) : (_jsx(SeoEditorPane, { entityType: "page", entityId: page.id, slug: page.slug, auditSections: page.sections, onAuditChange: setLiveAudit, contentHtml: seoContentHtml, draftMeta: {
761
+ seoTitle: page.seoTitle,
762
+ seoDescription: page.seoDescription,
763
+ }, onDraftChange: handlePageSettings, onClose: () => setRailTab('sections'), onSaved: handleSeoSaved })) }), historyOpen && (_jsx(VersionHistoryPanel, { versions: versions, loading: versionsLoading, error: versionsError, selectedIndex: versionIndex, onSelect: setVersionIndex, onClose: closeHistory, onRestore: handleRestoreVersion, restoring: restoring, canRestore: canRestoreVersion }))] })) }), _jsx("div", { className: "flex-1 overflow-hidden", children: sitePreview.active ? (_jsx(SitePreviewFrame, { src: sitePreview.url, viewport: viewport, reloadKey: sitePreview.reloadKey, title: `Site preview: ${page.title || 'Untitled page'}`, selectedId: selectedId, sections: page.sections, suppressSectionsEcho: suppressSectionsEcho, onSelectSection: handleSelectSection, onCanvasEdit: handleCanvasEdit, onCanvasRequestMedia: handleCanvasRequestMedia })) : (_jsx(EditorCanvas, { sections: canvasSections, selectedId: selectedId, breakpoint: viewport, zoom: zoom, onScaleChange: setCanvasScale, onCanvasElement: setCanvasEl, onSelect: handleSelectSection, structuralTypes: structuralTypes, sitePreviewAvailable: sitePreviewAvailable && !sitePreview.active, reviewing: reviewing, changedSectionIds: reviewChangedIds, reviewBar: reviewVersion ? (_jsx(ReviewBar, { version: reviewVersion, changedCount: reviewChangedIds.size, onRestore: () => void handleRestoreVersion(reviewVersion), onBackToCurrent: () => setVersionIndex(0), restoring: restoring, canRestore: canRestoreVersion })) : null })) }), selected && railTab === 'sections' && !reviewing && (_jsx(SectionInspector, { section: selected, canEdit: effectiveCanEdit, onClose: () => setSelectedId(null), onContentChange: handleContentChange, onSettingsChange: handleSettingsChange, onMetaChange: handleMetaChange, onToggleVisibility: () => handleToggleVisibility(selected.id) }))] }) })] }), _jsx(PageSettingsModal, { open: settingsOpen, page: page, canEdit: effectiveCanEdit, onClose: () => setSettingsOpen(false), onSave: handlePageSettings }), _jsx(ConfirmDialog, { open: confirmPublish, onClose: () => setConfirmPublish(false), onConfirm: doPublish, title: "Publish this page?", description: "The current content will go live and replace the published version.", confirmLabel: "Publish" }), page.id && (_jsx(SchedulePublishDialog, { collectionSlug: PAGES_COLLECTION, documentId: page.id, scheduledAt: page.scheduledAt, scheduledUnpublishAt: page.scheduledUnpublishAt, open: scheduleOpen, onClose: () => setScheduleOpen(false), onScheduled: handleScheduled })), _jsx(ConfirmDialog, { open: leaveTarget !== null, onClose: () => setLeaveTarget(null), onConfirm: () => {
459
764
  const target = leaveTarget;
460
765
  setLeaveTarget(null);
461
766
  setDirty(false);