@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
@@ -0,0 +1,226 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
4
+ import { DEFAULT_DIRECTIVES, type RobotsPermissions } from '@prenta/core/seo/robots-directives'
5
+ import { PageTab } from '../../views/page-editor/PageTab.js'
6
+ import type { EditorPage } from '../../lib/page-editor-service.js'
7
+
8
+ const page: EditorPage = {
9
+ id: 'p1',
10
+ title: 'About Us',
11
+ slug: 'about',
12
+ path: '/about',
13
+ status: 'PUBLISHED',
14
+ parentPageId: null,
15
+ sections: [],
16
+ seoTitle: '',
17
+ seoDescription: '',
18
+ publishedAt: '2026-05-15T10:00:00.000Z',
19
+ updatedAt: '2026-06-01T09:30:00.000Z',
20
+ scheduledAt: null,
21
+ scheduledUnpublishAt: null,
22
+ authorName: '',
23
+ authorId: '',
24
+ builderOwned: false,
25
+ }
26
+
27
+ function renderTab(
28
+ overrides: {
29
+ page?: Partial<EditorPage>
30
+ directives?: Partial<RobotsPermissions>
31
+ publishedPath?: string | null
32
+ onDirectivesChange?: (d: RobotsPermissions) => void
33
+ onPageChange?: (p: Partial<EditorPage>) => void
34
+ } = {},
35
+ ) {
36
+ return render(
37
+ <PageTab
38
+ page={{ ...page, ...overrides.page }}
39
+ canEdit
40
+ publishedPath={overrides.publishedPath === undefined ? '/about' : overrides.publishedPath}
41
+ siteUrl="https://example.com"
42
+ directives={{ ...DEFAULT_DIRECTIVES, ...overrides.directives }}
43
+ onDirectivesChange={overrides.onDirectivesChange ?? (() => {})}
44
+ redirectOnPublish
45
+ onRedirectOnPublishChange={() => {}}
46
+ onPageChange={overrides.onPageChange ?? (() => {})}
47
+ onDuplicate={() => {}}
48
+ onTrash={() => {}}
49
+ />,
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Only Address and Publishing are open by default; the rest are collapsed, and
55
+ * Radix unmounts closed content. Tests that assert on a collapsed group must
56
+ * open it first — which is also the interaction a real editor performs.
57
+ */
58
+ function openGroup(name: RegExp) {
59
+ fireEvent.click(screen.getByRole('button', { name }))
60
+ }
61
+
62
+ describe('PageTab — address', () => {
63
+ it('says nothing about redirects while the page has not moved', () => {
64
+ renderTab()
65
+ expect(screen.queryByText(/URL CHANGED/)).toBeNull()
66
+ expect(screen.queryByText(/301 redirect/)).toBeNull()
67
+ cleanup()
68
+ })
69
+
70
+ it('offers a 301 naming both paths once a live page moves', () => {
71
+ renderTab({ page: { path: '/about-us' } })
72
+ expect(screen.getByText('URL CHANGED')).toBeTruthy()
73
+ expect(screen.getByText(/Create a 301 redirect on publish/)).toBeTruthy()
74
+ expect(screen.getByText('/about → /about-us')).toBeTruthy()
75
+ cleanup()
76
+ })
77
+
78
+ it('keeps path in lockstep when the slug was driving it', () => {
79
+ // The displayed URL and the 301 detector both read `path`. Updating only
80
+ // `slug` left a live `/about` → slug `about-us` with no warning and a
81
+ // full URL that still said /about.
82
+ const onPageChange = vi.fn()
83
+ renderTab({ onPageChange })
84
+ fireEvent.change(screen.getByLabelText(/URL slug/i), { target: { value: 'about-us' } })
85
+ expect(onPageChange).toHaveBeenCalledWith({ slug: 'about-us', path: '/about-us' })
86
+ cleanup()
87
+ })
88
+
89
+ it('leaves a custom path alone when the slug changes', () => {
90
+ const onPageChange = vi.fn()
91
+ renderTab({ page: { slug: 'about', path: '/team/about' }, onPageChange })
92
+ fireEvent.change(screen.getByLabelText(/URL slug/i), { target: { value: 'about-us' } })
93
+ expect(onPageChange).toHaveBeenCalledWith({ slug: 'about-us' })
94
+ cleanup()
95
+ })
96
+
97
+ it('warns without promising a 301 when the user cannot create redirects', () => {
98
+ render(
99
+ <PageTab
100
+ page={{ ...page, path: '/about-us' }}
101
+ canEdit
102
+ publishedPath="/about"
103
+ siteUrl="https://example.com"
104
+ directives={DEFAULT_DIRECTIVES}
105
+ onDirectivesChange={() => {}}
106
+ redirectOnPublish
107
+ onRedirectOnPublishChange={() => {}}
108
+ canCreateRedirect={false}
109
+ onPageChange={() => {}}
110
+ onDuplicate={() => {}}
111
+ onTrash={() => {}}
112
+ />,
113
+ )
114
+ expect(screen.getByText('URL CHANGED')).toBeTruthy()
115
+ expect(screen.queryByText(/Create a 301 redirect on publish/)).toBeNull()
116
+ expect(screen.getByText(/Ask an admin to create a 301/)).toBeTruthy()
117
+ cleanup()
118
+ })
119
+
120
+ it('stays quiet when a page that was never live changes its slug', () => {
121
+ // Nothing points at the old path. Warning here would train editors to
122
+ // dismiss the warning that matters.
123
+ renderTab({ page: { path: '/about-us', status: 'DRAFT' }, publishedPath: null })
124
+ expect(screen.queryByText(/URL CHANGED/)).toBeNull()
125
+ cleanup()
126
+ })
127
+ })
128
+
129
+ describe('PageTab — search directives', () => {
130
+ it('summarises how many directives are restricted', () => {
131
+ renderTab()
132
+ expect(screen.getByText('OPEN')).toBeTruthy()
133
+ cleanup()
134
+
135
+ renderTab({ directives: { index: false, snippet: false } })
136
+ // `noindex` subsumes the snippet token, but both are still restrictions the
137
+ // editor chose, so the count reports two.
138
+ expect(screen.getByText('2 RESTRICTED')).toBeTruthy()
139
+ cleanup()
140
+ })
141
+
142
+ it('previews the exact head line, composed by the renderer’s own function', () => {
143
+ renderTab({ directives: { index: false } })
144
+ openGroup(/Search directives/)
145
+ expect(screen.getByText('<meta name="robots" content="noindex" />')).toBeTruthy()
146
+ cleanup()
147
+
148
+ renderTab()
149
+ openGroup(/Search directives/)
150
+ // Nothing is emitted when everything is permissive, and the preview says
151
+ // so rather than showing a blank box.
152
+ expect(screen.getByText(/no robots meta/)).toBeTruthy()
153
+ cleanup()
154
+ })
155
+
156
+ it('shows each switch’s literal directive token', () => {
157
+ // The failure this prevents is flipping a switch whose consequence you
158
+ // could not name. `noindex` beside it makes the consequence the label.
159
+ renderTab({ directives: { index: false } })
160
+ openGroup(/Search directives/)
161
+ expect(screen.getByText('noindex')).toBeTruthy()
162
+ expect(screen.getByText('follow')).toBeTruthy()
163
+ cleanup()
164
+ })
165
+
166
+ it('disables snippet and archive under noindex', () => {
167
+ // They have no effect once the page is out of the index, and an enabled
168
+ // control that changes nothing is a lie about what it does.
169
+ renderTab({ directives: { index: false } })
170
+ openGroup(/Search directives/)
171
+ expect(
172
+ screen.getByRole('switch', { name: /Allow text snippets/ }).hasAttribute('disabled'),
173
+ ).toBe(true)
174
+ expect(
175
+ screen.getByRole('switch', { name: /Allow cached copies/ }).hasAttribute('disabled'),
176
+ ).toBe(true)
177
+ cleanup()
178
+ })
179
+
180
+ it('warns that publishing will not make a noindexed page findable', () => {
181
+ renderTab({ directives: { index: false } })
182
+ openGroup(/Search directives/)
183
+ expect(screen.getByText(/hidden from search/)).toBeTruthy()
184
+ cleanup()
185
+ })
186
+
187
+ it('reports a toggle as a permission, not a restriction', () => {
188
+ const onDirectivesChange = vi.fn()
189
+ renderTab({ onDirectivesChange })
190
+ openGroup(/Search directives/)
191
+ fireEvent.click(screen.getByRole('switch', { name: /Show in search results/ }))
192
+ expect(onDirectivesChange).toHaveBeenCalledWith(
193
+ expect.objectContaining({ index: false, follow: true }),
194
+ )
195
+ cleanup()
196
+ })
197
+ })
198
+
199
+ describe('PageTab — danger zone', () => {
200
+ it('asks before trashing, and says when the page is live', () => {
201
+ const onTrash = vi.fn()
202
+ render(
203
+ <PageTab
204
+ page={page}
205
+ canEdit
206
+ publishedPath="/about"
207
+ siteUrl="https://example.com"
208
+ directives={DEFAULT_DIRECTIVES}
209
+ onDirectivesChange={() => {}}
210
+ redirectOnPublish
211
+ onRedirectOnPublishChange={() => {}}
212
+ onPageChange={() => {}}
213
+ onDuplicate={() => {}}
214
+ onTrash={onTrash}
215
+ />,
216
+ )
217
+ openGroup(/Danger zone/)
218
+ fireEvent.click(screen.getByRole('button', { name: /Move to trash/ }))
219
+ expect(onTrash).not.toHaveBeenCalled()
220
+ expect(screen.getByText(/It is live, and will stop being served/)).toBeTruthy()
221
+
222
+ fireEvent.click(screen.getByRole('button', { name: 'Move to trash' }))
223
+ expect(onTrash).toHaveBeenCalled()
224
+ cleanup()
225
+ })
226
+ })
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ paddingClass as sitePadding,
4
+ backgroundClass as siteBackground,
5
+ isDark as siteIsDark,
6
+ } from '@prenta/sections-react'
7
+ import {
8
+ paddingClass as adminPadding,
9
+ backgroundClass as adminBackground,
10
+ isDarkBackground as adminIsDark,
11
+ } from '../../views/page-editor/sections/parts.js'
12
+ import { BACKGROUND_PRESETS, backgroundPreset } from '@prenta/core/page-sections'
13
+ import type { SectionSettings } from '@prenta/core/page-sections'
14
+
15
+ /**
16
+ * The editor canvas wraps each section itself rather than using `<Sections>`,
17
+ * so the admin applies ITS wrapper classes around a body rendered by
18
+ * sections-react. Two implementations, one rendered result.
19
+ *
20
+ * They are duplicated because they have to be: Tailwind generates utilities by
21
+ * scanning source text, and each package's stylesheet scans only its own `src/`
22
+ * tree. A literal shared from a third package is generated by nobody, and the
23
+ * section renders with no background — silently, with correct markup. So
24
+ * `@prenta/core` owns the DECISION (`backgroundPreset`) and each side maps it
25
+ * to its own strings; this file asserts the strings stayed in step.
26
+ */
27
+ const settings = (over: Partial<SectionSettings> = {}) => over as SectionSettings
28
+
29
+ /** One settings object per preset, so coverage follows the preset list. */
30
+ const SETTINGS_FOR: Record<string, SectionSettings> = {
31
+ 'gradient-purple': settings({ backgroundType: 'gradient', backgroundColor: 'purple' }),
32
+ 'solid-purple': settings({ backgroundType: 'solid', backgroundColor: 'purple' }),
33
+ 'solid-muted': settings({ backgroundType: 'solid', backgroundColor: 'muted' }),
34
+ plain: settings({ backgroundType: 'none' }),
35
+ }
36
+
37
+ describe('section wrapper classes', () => {
38
+ it('pads identically in the canvas and on the site', () => {
39
+ for (const paddingY of ['sm', 'md', 'lg', 'xl', undefined]) {
40
+ const s = settings({ paddingY } as Partial<SectionSettings>)
41
+ expect(adminPadding(s), `paddingY: ${paddingY}`).toBe(sitePadding(s as never))
42
+ }
43
+ })
44
+
45
+ it('pads with container steps, never viewport breakpoints', () => {
46
+ // A viewport step here would reflow the canvas against the editor window
47
+ // and undo the whole point of the container migration.
48
+ const xl = adminPadding(settings({ paddingY: 'xl' } as Partial<SectionSettings>))
49
+ expect(xl).toContain('@pv-')
50
+ expect(xl).not.toMatch(/(^|\s)(sm|md|lg|xl):/)
51
+ })
52
+
53
+ it('covers every declared preset', () => {
54
+ // Guards the fixture table itself: add a preset in core and this fails
55
+ // until it has settings here, so the assertions below cannot quietly stop
56
+ // covering it.
57
+ for (const preset of BACKGROUND_PRESETS) {
58
+ const s = SETTINGS_FOR[preset]
59
+ expect(s, `no settings fixture for preset "${preset}"`).toBeDefined()
60
+ expect(backgroundPreset(s!), `settings for ${preset}`).toBe(preset)
61
+ }
62
+ })
63
+
64
+ it('paints every preset identically in the canvas and on the site', () => {
65
+ // The canvas is a preview of the public page. It previously used admin
66
+ // theme tokens, so in dark mode it showed a hero in `#9D71F0` on dark body
67
+ // sections while the live page rendered violet-600 on white — a page no
68
+ // visitor would ever see.
69
+ for (const preset of BACKGROUND_PRESETS) {
70
+ const s = SETTINGS_FOR[preset]!
71
+ expect(adminBackground(s), `background for ${preset}`).toBe(siteBackground(s as never))
72
+ }
73
+ })
74
+
75
+ it('never paints the canvas with admin theme tokens', () => {
76
+ // The specific regression: theme tokens follow the editor's light/dark
77
+ // preference, and the live site does not.
78
+ for (const preset of BACKGROUND_PRESETS) {
79
+ const cls = adminBackground(SETTINGS_FOR[preset]!)
80
+ expect(cls, preset).not.toMatch(/\b(bg-card|bg-brand|bg-muted|text-foreground)\b/)
81
+ }
82
+ })
83
+
84
+ it('agrees on which backgrounds are dark', () => {
85
+ for (const preset of BACKGROUND_PRESETS) {
86
+ const s = SETTINGS_FOR[preset]!
87
+ expect(adminIsDark(s), preset).toBe(siteIsDark(s as never))
88
+ }
89
+ })
90
+
91
+ it('falls back to the plain preset for an unrecognised colour', () => {
92
+ // A consumer can put any string in `backgroundColor`. It must land on the
93
+ // default background rather than losing the background rules entirely.
94
+ const odd = settings({ backgroundType: 'solid', backgroundColor: 'chartreuse' })
95
+ expect(backgroundPreset(odd)).toBe('plain')
96
+ expect(adminBackground(odd)).toBe(siteBackground(odd as never))
97
+ })
98
+ })
@@ -0,0 +1,104 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { render, screen } from '@testing-library/react'
4
+ import { SectionsPanel } from '../../views/page-editor/SectionsPanel.js'
5
+ import type { PageSection } from '../../lib/page-editor-service.js'
6
+
7
+ /**
8
+ * Pages and posts are design-owned: the section list comes from the template in
9
+ * code. `SectionsPanel` therefore defaults to read-only, and only the template
10
+ * editor — the surface that authors a template — passes `structural`.
11
+ *
12
+ * The default direction is the point. A future consumer that forgets the flag
13
+ * must get the safe behaviour, not an editor that can silently restructure a
14
+ * design-owned page.
15
+ */
16
+
17
+ function section(over: Partial<PageSection> = {}): PageSection {
18
+ return {
19
+ id: 's1',
20
+ sectionType: 'hero',
21
+ name: 'Hero Banner',
22
+ visible: true,
23
+ content: {},
24
+ settings: {},
25
+ ...over,
26
+ } as PageSection
27
+ }
28
+
29
+ function renderPanel(over: Partial<Parameters<typeof SectionsPanel>[0]> = {}) {
30
+ return render(
31
+ <SectionsPanel
32
+ sections={[section(), section({ id: 's2', name: 'Mission' })]}
33
+ selectedId={null}
34
+ canEdit
35
+ onSelect={vi.fn()}
36
+ onToggleVisibility={vi.fn()}
37
+ {...over}
38
+ />,
39
+ )
40
+ }
41
+
42
+ describe('SectionsPanel — design-owned by default', () => {
43
+ it('offers no add, reorder, duplicate, or delete', () => {
44
+ renderPanel()
45
+ expect(screen.queryByRole('button', { name: /add section/i })).toBeNull()
46
+ expect(screen.queryByRole('button', { name: /drag to reorder/i })).toBeNull()
47
+ expect(screen.queryByRole('button', { name: /more actions/i })).toBeNull()
48
+ })
49
+
50
+ it('keeps the visibility toggle — the one structural property an editor owns', () => {
51
+ renderPanel()
52
+ expect(screen.getByRole('button', { name: 'Hide Hero Banner' })).toBeTruthy()
53
+ })
54
+
55
+ it('keeps sections selectable for editing', () => {
56
+ const onSelect = vi.fn()
57
+ renderPanel({ onSelect })
58
+ screen.getByText('Hero Banner').click()
59
+ expect(onSelect).toHaveBeenCalledWith('s1')
60
+ })
61
+
62
+ it('explains the absence rather than looking broken', () => {
63
+ // Without this the panel reads as a regression — "where did Add Section go"
64
+ // is the first thing a returning editor asks.
65
+ renderPanel()
66
+ expect(screen.getByText(/Layout is design-owned/i)).toBeTruthy()
67
+ expect(screen.getByText(/Hide a section to drop it from the live page/i)).toBeTruthy()
68
+ })
69
+
70
+ it('says the template renders nothing rather than inviting a first section', () => {
71
+ renderPanel({ sections: [] })
72
+ expect(screen.getByText(/template renders no sections/i)).toBeTruthy()
73
+ expect(screen.queryByText(/add one below/i)).toBeNull()
74
+ })
75
+ })
76
+
77
+ describe('SectionsPanel — structural opt-in', () => {
78
+ it('restores every structural affordance when asked', () => {
79
+ renderPanel({
80
+ structural: true,
81
+ onReorder: vi.fn(),
82
+ onDuplicate: vi.fn(),
83
+ onRequestDelete: vi.fn(),
84
+ onAdd: vi.fn(),
85
+ })
86
+ expect(screen.getByRole('button', { name: /add section/i })).toBeTruthy()
87
+ expect(screen.getAllByRole('button', { name: /drag to reorder/i })).toHaveLength(2)
88
+ expect(screen.getAllByRole('button', { name: /more actions/i })).toHaveLength(2)
89
+ expect(screen.queryByText(/Layout is design-owned/i)).toBeNull()
90
+ })
91
+
92
+ it('still locks externally-managed sections', () => {
93
+ // `unmanaged` sections are read-only even where structural editing is
94
+ // allowed — they are owned by a system outside the CMS.
95
+ renderPanel({
96
+ structural: true,
97
+ sections: [section({ unmanaged: true } as Partial<PageSection>)],
98
+ onReorder: vi.fn(),
99
+ onAdd: vi.fn(),
100
+ })
101
+ expect(screen.queryByRole('button', { name: /more actions/i })).toBeNull()
102
+ expect(screen.getByLabelText(/managed outside the editor/i)).toBeTruthy()
103
+ })
104
+ })
@@ -0,0 +1,195 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'
4
+ import { VersionHistoryPanel } from '../../views/page-editor/VersionHistoryPanel.js'
5
+ import type { PageVersion } from '../../lib/version-history.js'
6
+ import type { PageSection } from '../../lib/page-editor-service.js'
7
+
8
+ afterEach(cleanup)
9
+
10
+ function section(id: string, content: Record<string, unknown>): PageSection {
11
+ return { id, sectionType: 'hero', name: 'Hero Banner', visible: true, content, settings: {} }
12
+ }
13
+
14
+ const NOW = new Date()
15
+ const iso = (hoursAgo: number) => new Date(NOW.getTime() - hoursAgo * 3_600_000).toISOString()
16
+
17
+ const VERSIONS: PageVersion[] = [
18
+ {
19
+ id: 'v3',
20
+ changeType: 'PUBLISH',
21
+ createdAt: iso(1),
22
+ author: { id: 'u1', name: 'Sarah Mills' },
23
+ sections: [section('s1', { heading: 'Third headline' })],
24
+ },
25
+ {
26
+ id: 'v2',
27
+ changeType: 'UPDATE',
28
+ createdAt: iso(2),
29
+ author: { id: 'u2', name: 'Dan Ito' },
30
+ sections: [section('s1', { heading: 'Second headline' })],
31
+ },
32
+ {
33
+ id: 'v1',
34
+ changeType: 'CREATE',
35
+ createdAt: iso(30),
36
+ author: { id: 'u1', name: 'Sarah Mills' },
37
+ sections: [section('s1', { heading: 'First headline' })],
38
+ },
39
+ ]
40
+
41
+ function renderPanel(props: Partial<React.ComponentProps<typeof VersionHistoryPanel>> = {}) {
42
+ const onSelect = vi.fn()
43
+ const onRestore = vi.fn()
44
+ const onClose = vi.fn()
45
+ render(
46
+ <VersionHistoryPanel
47
+ versions={VERSIONS}
48
+ loading={false}
49
+ error={null}
50
+ selectedIndex={0}
51
+ onSelect={onSelect}
52
+ onClose={onClose}
53
+ onRestore={onRestore}
54
+ restoring={false}
55
+ canRestore
56
+ {...props}
57
+ />,
58
+ )
59
+ return { onSelect, onRestore, onClose }
60
+ }
61
+
62
+ describe('VersionHistoryPanel', () => {
63
+ it('groups entries under sticky day headings', () => {
64
+ renderPanel()
65
+ expect(screen.getByRole('heading', { name: 'Today' })).toBeTruthy()
66
+ // v1 is 30 hours back, so it lands on its own day.
67
+ expect(screen.getAllByRole('heading', { level: 3 }).length).toBeGreaterThan(1)
68
+ })
69
+
70
+ it('marks the newest entry as current and shows the published pill', () => {
71
+ renderPanel()
72
+ expect(screen.getByText('Current')).toBeTruthy()
73
+ expect(screen.getByText('Published')).toBeTruthy()
74
+ // No pill claims "Draft" — nothing in this codebase records one.
75
+ expect(screen.queryByText('Draft')).toBeNull()
76
+ })
77
+
78
+ it('summarises each entry by what that save changed', () => {
79
+ renderPanel()
80
+ // v3 changed one field against v2.
81
+ expect(screen.getByText(/Sarah Mills · 1 change$/)).toBeTruthy()
82
+ // v1 has no predecessor in the list, so it reports no computed changes
83
+ // rather than claiming it created everything.
84
+ expect(screen.getByText(/Sarah Mills · no content changes/)).toBeTruthy()
85
+ })
86
+
87
+ it('shows the computed field diff, old struck through and new after it', () => {
88
+ renderPanel()
89
+ // The newest changed entry auto-expands.
90
+ expect(screen.getByText('Heading')).toBeTruthy()
91
+ const oldValue = screen.getByText('Second headline')
92
+ const newValue = screen.getByText('Third headline')
93
+ expect(oldValue.className).toContain('line-through')
94
+ expect(newValue.className).not.toContain('line-through')
95
+ })
96
+
97
+ it('selects the version when its row is activated, which is what drives the canvas', () => {
98
+ const { onSelect } = renderPanel()
99
+ fireEvent.click(screen.getByText(/Dan Ito/))
100
+ expect(onSelect).toHaveBeenCalledWith(1)
101
+ })
102
+
103
+ it('offers Restore only on the selected older row', () => {
104
+ renderPanel({ selectedIndex: 0 })
105
+ // Selected row is the current one — restoring it is a no-op.
106
+ expect(screen.queryByRole('button', { name: /Restore this version/ })).toBeNull()
107
+
108
+ cleanup()
109
+ renderPanel({ selectedIndex: 1 })
110
+ const restore = screen.getByRole('button', { name: /Restore this version/ })
111
+ expect(restore).toBeTruthy()
112
+ })
113
+
114
+ it('shows the diff for whichever version is selected, including one selected from outside', () => {
115
+ // The review bar and the canvas both move the selection without the panel
116
+ // being clicked. When disclosure was tracked separately from selection,
117
+ // the diff described one version while the canvas rendered another — and
118
+ // Restore sat on a row that was not open.
119
+ renderPanel({ selectedIndex: 1 })
120
+ expect(screen.getByText('First headline')).toBeTruthy()
121
+ expect(screen.getByText('Second headline')).toBeTruthy()
122
+ expect(screen.queryByText('Third headline')).toBeNull()
123
+ expect(screen.getByRole('button', { name: /Restore this version/ })).toBeTruthy()
124
+ })
125
+
126
+ it('collapses the row it is already on rather than reselecting it', () => {
127
+ const { onSelect } = renderPanel({ selectedIndex: 0 })
128
+ expect(screen.getByText('Heading')).toBeTruthy()
129
+
130
+ fireEvent.click(screen.getByText(/Sarah Mills · 1 change/))
131
+
132
+ expect(onSelect).not.toHaveBeenCalled()
133
+ expect(screen.queryByText('Heading')).toBeNull()
134
+ })
135
+
136
+ it('can restore the oldest version, which by definition has no diff to show', () => {
137
+ // Restore used to live inside the diff block, so a version with no
138
+ // computed changes — the oldest one held, or a metadata-only save — was
139
+ // reviewable and not restorable. That is the wrong way round: the oldest
140
+ // version is the one a rollback most often wants.
141
+ renderPanel({ selectedIndex: 2 })
142
+
143
+ expect(screen.getByText(/No section changes to show/)).toBeTruthy()
144
+ expect(screen.getByRole('button', { name: /Restore this version/ })).toBeTruthy()
145
+ })
146
+
147
+ it('hides Restore from a user who cannot edit', () => {
148
+ renderPanel({ selectedIndex: 1, canRestore: false })
149
+ expect(screen.queryByRole('button', { name: /Restore this version/ })).toBeNull()
150
+ })
151
+
152
+ it('restores the version whose row was clicked', () => {
153
+ const { onRestore } = renderPanel({ selectedIndex: 1 })
154
+ fireEvent.click(screen.getByRole('button', { name: /Restore this version/ }))
155
+ expect(onRestore).toHaveBeenCalledWith(expect.objectContaining({ id: 'v2' }))
156
+ })
157
+
158
+ it('renders two non-adjacent groups sharing a day label without colliding', () => {
159
+ // `groupVersionsByDay` walks CONSECUTIVE runs, so an out-of-order page
160
+ // legitimately yields ['Today', 'Yesterday', 'Today'] — its own test
161
+ // asserts that. Keyed on the label, the second "Today" collides with the
162
+ // first and React mis-associates the two sections' contents.
163
+ const outOfOrder: PageVersion[] = [
164
+ { ...VERSIONS[0]!, id: 'a', createdAt: iso(1) },
165
+ { ...VERSIONS[1]!, id: 'b', createdAt: iso(30) },
166
+ { ...VERSIONS[2]!, id: 'c', createdAt: iso(2) },
167
+ ]
168
+
169
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
170
+ renderPanel({ versions: outOfOrder })
171
+
172
+ expect(screen.getAllByRole('heading', { name: 'Today' })).toHaveLength(2)
173
+ expect(
174
+ spy.mock.calls.some((call) => String(call[0]).includes('same key')),
175
+ 'React reported duplicate keys',
176
+ ).toBe(false)
177
+ spy.mockRestore()
178
+ })
179
+
180
+ it('reports an empty history rather than an empty box', () => {
181
+ renderPanel({ versions: [] })
182
+ expect(screen.getByText(/No versions yet/)).toBeTruthy()
183
+ })
184
+
185
+ it('surfaces a load failure', () => {
186
+ renderPanel({ versions: [], error: 'Network unreachable' })
187
+ expect(screen.getByText('Network unreachable')).toBeTruthy()
188
+ })
189
+
190
+ it('announces itself as a named region so it is reachable without sight of the overlay', () => {
191
+ renderPanel()
192
+ const region = screen.getByRole('region', { name: 'Version history' })
193
+ expect(within(region).getByRole('button', { name: 'Close version history' })).toBeTruthy()
194
+ })
195
+ })