@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,84 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ BREAKPOINTS,
4
+ DEFAULT_BREAKPOINT,
5
+ breakpointsInGroup,
6
+ coerceBreakpointId,
7
+ fitScale,
8
+ getBreakpoint,
9
+ } from '../../lib/breakpoints.js'
10
+
11
+ describe('breakpoint presets', () => {
12
+ it('renders 4K at the CSS width such displays report, not the physical one', () => {
13
+ // 3840 is the pixel count; 2560 is what a stylesheet sees once DPR is
14
+ // applied. Rendering at 3840 would put the canvas in a container-query
15
+ // bucket no real visitor is ever in.
16
+ expect(getBreakpoint('4k').width).toBe(2560)
17
+ })
18
+
19
+ it('has exactly one preset above the 2000px container-query step', () => {
20
+ // The canvas's `@container (min-width: 2000px)` branch scales type and
21
+ // padding up. If no preset exercised it, the branch would be untestable
22
+ // from the editor.
23
+ const above = BREAKPOINTS.filter((b) => b.width >= 2000)
24
+ expect(above.map((b) => b.id)).toEqual(['4k'])
25
+ })
26
+
27
+ it('covers every container-query step with a distinct width', () => {
28
+ const widths = BREAKPOINTS.map((b) => b.width)
29
+ expect(widths).toEqual([2560, 1920, 1440, 1024, 768, 390])
30
+ expect(new Set(widths).size).toBe(widths.length)
31
+ })
32
+
33
+ it('groups presets in menu order', () => {
34
+ expect(breakpointsInGroup('DESKTOP').map((b) => b.id)).toEqual(['4k', 'wide', 'laptop'])
35
+ expect(breakpointsInGroup('TABLET').map((b) => b.id)).toEqual(['tabletL', 'tabletP'])
36
+ expect(breakpointsInGroup('MOBILE').map((b) => b.id)).toEqual(['mobile'])
37
+ })
38
+
39
+ it('falls back to the default for an unknown id', () => {
40
+ expect(getBreakpoint('nope' as never).id).toBe(DEFAULT_BREAKPOINT)
41
+ })
42
+ })
43
+
44
+ describe('coerceBreakpointId', () => {
45
+ it('maps retired v0.92 ids onto their equivalent width', () => {
46
+ // `1080p` was 1440px and `tablet` was 768px. Mapping by width keeps an
47
+ // editor that was open across the upgrade on the same preview.
48
+ expect(getBreakpoint(coerceBreakpointId('1080p')).width).toBe(1440)
49
+ expect(getBreakpoint(coerceBreakpointId('tablet')).width).toBe(768)
50
+ })
51
+
52
+ it('passes through current ids and defaults anything else', () => {
53
+ expect(coerceBreakpointId('tabletL')).toBe('tabletL')
54
+ expect(coerceBreakpointId('garbage')).toBe(DEFAULT_BREAKPOINT)
55
+ expect(coerceBreakpointId(undefined)).toBe(DEFAULT_BREAKPOINT)
56
+ expect(coerceBreakpointId(1920)).toBe(DEFAULT_BREAKPOINT)
57
+ })
58
+ })
59
+
60
+ describe('fitScale', () => {
61
+ it('shrinks a wide breakpoint to the available width', () => {
62
+ expect(fitScale(800, 1600)).toBe(0.5)
63
+ })
64
+
65
+ it('never scales above 1', () => {
66
+ // A 390px mobile canvas stretched across a 1200px column would render text
67
+ // at 3x the size a visitor sees — worse than useless as a proof.
68
+ expect(fitScale(1200, 390)).toBe(1)
69
+ })
70
+
71
+ it('returns 1 at 100% zoom regardless of available width', () => {
72
+ expect(fitScale(400, 2560, 1)).toBe(1)
73
+ })
74
+
75
+ it('returns 1 rather than 0 or NaN before the first measurement', () => {
76
+ // The sentinel reads 0 on the very first paint. Propagating that would
77
+ // produce `scale(0)` and a canvas that measures as zero-height, which then
78
+ // feeds back into the next measurement.
79
+ expect(fitScale(0, 1920)).toBe(1)
80
+ expect(fitScale(Number.NaN, 1920)).toBe(1)
81
+ expect(fitScale(-10, 1920)).toBe(1)
82
+ expect(fitScale(800, 0)).toBe(1)
83
+ })
84
+ })
@@ -0,0 +1,56 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import {
4
+ CANVAS_EDIT_TIP_STORAGE_KEY,
5
+ dismissCanvasEditTip,
6
+ injectPreviewEditChrome,
7
+ isCanvasEditTipDismissed,
8
+ PREVIEW_EDITABLE_FIELD_CHROME_CSS,
9
+ } from '../../lib/canvas-edit-chrome.js'
10
+
11
+ // happy-dom localStorage is non-functional in this Node setup — stub a Map.
12
+ const storage = new Map<string, string>()
13
+ vi.stubGlobal('localStorage', {
14
+ getItem: (key: string) => storage.get(key) ?? null,
15
+ setItem: (key: string, value: string) => void storage.set(key, String(value)),
16
+ removeItem: (key: string) => void storage.delete(key),
17
+ clear: () => storage.clear(),
18
+ })
19
+
20
+ describe('canvas-edit-chrome', () => {
21
+ afterEach(() => {
22
+ storage.clear()
23
+ })
24
+
25
+ it('tracks tip dismiss in localStorage', () => {
26
+ expect(isCanvasEditTipDismissed()).toBe(false)
27
+ dismissCanvasEditTip()
28
+ expect(isCanvasEditTipDismissed()).toBe(true)
29
+ expect(localStorage.getItem(CANVAS_EDIT_TIP_STORAGE_KEY)).toBe('1')
30
+ })
31
+
32
+ it('injects editable field chrome into a same-origin iframe document', () => {
33
+ const iframe = document.createElement('iframe')
34
+ document.body.appendChild(iframe)
35
+ const doc = iframe.contentDocument
36
+ expect(doc).toBeTruthy()
37
+ if (!doc) return
38
+
39
+ expect(injectPreviewEditChrome(iframe)).toBe(true)
40
+ const style = doc.head.querySelector('style[data-prenta-edit-chrome]')
41
+ expect(style?.textContent).toContain('[data-cms-field]')
42
+ expect(style?.textContent).toContain('data-cms-field-kind')
43
+ expect(PREVIEW_EDITABLE_FIELD_CHROME_CSS.length).toBeGreaterThan(0)
44
+ expect(doc.documentElement.getAttribute('data-prenta-preview-editing')).toBe('true')
45
+
46
+ // Refresh is idempotent — one style tag.
47
+ expect(injectPreviewEditChrome(iframe)).toBe(true)
48
+ expect(doc.head.querySelectorAll('style[data-prenta-edit-chrome]')).toHaveLength(1)
49
+
50
+ iframe.remove()
51
+ })
52
+
53
+ it('returns false when iframe has no contentDocument', () => {
54
+ expect(injectPreviewEditChrome(null)).toBe(false)
55
+ })
56
+ })
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ assetWash,
4
+ displayName,
5
+ formatBadge,
6
+ ratioLabel,
7
+ CROP_RATIO,
8
+ } from '../../lib/media-format.js'
9
+
10
+ describe('ratioLabel', () => {
11
+ it('names the ratios an editor recognises', () => {
12
+ expect(ratioLabel(1920, 1080)).toBe('16:9')
13
+ expect(ratioLabel(2400, 1600)).toBe('3:2')
14
+ expect(ratioLabel(1200, 630)).toBe('1.91:1')
15
+ expect(ratioLabel(800, 800)).toBe('1:1')
16
+ expect(ratioLabel(1080, 1350)).toBe('4:5')
17
+ expect(ratioLabel(1080, 1920)).toBe('9:16')
18
+ })
19
+
20
+ it('tolerates an off-by-a-pixel scan', () => {
21
+ // 2401×1600 is 1.5006, not 1.5. An editor should still read "3:2" rather
22
+ // than "1.50:1" — the tolerance is what makes the label useful on real
23
+ // assets instead of only on synthetic ones.
24
+ expect(ratioLabel(2401, 1600)).toBe('3:2')
25
+ })
26
+
27
+ it('reports the number when nothing named is close', () => {
28
+ expect(ratioLabel(1000, 300)).toBe('3.33:1')
29
+ })
30
+
31
+ it('says nothing when dimensions are unknown', () => {
32
+ // The library does not always carry width/height. Inventing "1.00:1" would
33
+ // be a confident wrong answer about the one thing this label exists for.
34
+ expect(ratioLabel(undefined, undefined)).toBeNull()
35
+ expect(ratioLabel(0, 100)).toBeNull()
36
+ expect(ratioLabel(100, 0)).toBeNull()
37
+ })
38
+ })
39
+
40
+ describe('formatBadge', () => {
41
+ it('uses the file extension', () => {
42
+ expect(formatBadge('team-offsite.jpg')).toBe('JPG')
43
+ expect(formatBadge('/uploads/dashboard-hero.PNG')).toBe('PNG')
44
+ })
45
+
46
+ it('ignores a query string', () => {
47
+ expect(formatBadge('https://cdn.example.com/a/b.webp?token=abc123&x=1')).toBe('WEBP')
48
+ })
49
+
50
+ it('falls back to the MIME subtype when there is no extension', () => {
51
+ // Signed CDN links and upload routes often have no extension at all.
52
+ expect(formatBadge('https://cdn.example.com/asset/9f3a2', 'image/avif')).toBe('AVIF')
53
+ expect(formatBadge('https://cdn.example.com/asset/9f3a2', 'image/svg+xml')).toBe('SVG')
54
+ })
55
+
56
+ it('never renders an empty chip', () => {
57
+ expect(formatBadge('')).toBe('FILE')
58
+ expect(formatBadge('no-extension-here')).toBe('FILE')
59
+ })
60
+ })
61
+
62
+ describe('displayName', () => {
63
+ it('shows the filename, not the path or the token', () => {
64
+ expect(displayName('https://cdn.example.com/media/team-offsite-2026.jpg?sig=xyz')).toBe(
65
+ 'team-offsite-2026.jpg',
66
+ )
67
+ })
68
+
69
+ it('decodes an escaped name', () => {
70
+ expect(displayName('/media/team%20offsite.jpg')).toBe('team offsite.jpg')
71
+ })
72
+ })
73
+
74
+ describe('assetWash', () => {
75
+ it('gives the same asset the same colour every time', () => {
76
+ // Recognising a thumbnail depends on it not changing between renders.
77
+ expect(assetWash('hero.jpg')).toBe(assetWash('hero.jpg'))
78
+ expect(assetWash('hero.jpg')).not.toBe(assetWash('other.jpg'))
79
+ })
80
+ })
81
+
82
+ describe('CROP_RATIO', () => {
83
+ it('describes the crop the template renders, not the asset', () => {
84
+ // Showing the asset's own ratio would tell an editor their portrait photo
85
+ // is fine, then crop its subject out on the page.
86
+ expect(CROP_RATIO.section).toBe('16 / 10')
87
+ expect(CROP_RATIO.social).toBe('1.91 / 1')
88
+ })
89
+ })
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { auditPage } from '@prenta/core/seo/page-audit'
3
+ import type { PageSection } from '@prenta/core/page-sections'
4
+ import { buildPageAuditInput, metaFromRecord } from '../../lib/page-audit-input.js'
5
+ import type { SeoContentRecord } from '../../lib/seo-service.js'
6
+
7
+ /**
8
+ * Two callers build the audit — the page editor from the saved record (so the
9
+ * header chip has a number from first paint) and the SEO pane from live form
10
+ * state (so it moves as you type). They must produce the same score for the
11
+ * same data, or the editor is back to showing numbers that disagree.
12
+ */
13
+
14
+ const sections = [
15
+ {
16
+ id: 's1',
17
+ sectionType: 'hero',
18
+ name: 'Hero',
19
+ visible: true,
20
+ content: {
21
+ heading: 'Headless CMS for modern teams',
22
+ body: 'Prenta gives developers an API-first architecture and editors a design-owned authoring surface, so content ships without layout drift.',
23
+ imageAlt: 'A described image',
24
+ },
25
+ settings: {},
26
+ },
27
+ ] as unknown as PageSection[]
28
+
29
+ const record = {
30
+ title: 'About Us',
31
+ siteName: 'Prenta',
32
+ titleTemplate: '%page% | %site%',
33
+ url: '/about',
34
+ metaTitlePage: 'Headless CMS for teams',
35
+ metaDescription:
36
+ 'Prenta gives developers an API-first architecture and editors a design-owned authoring surface for shipping content at scale.',
37
+ focusKeyword: 'headless cms',
38
+ structuredDataType: 'WebPage',
39
+ noindex: false,
40
+ llmsInclude: 'AUTO',
41
+ ogImage: null,
42
+ } as unknown as SeoContentRecord
43
+
44
+ describe('page audit input', () => {
45
+ it('produces the same score from a record as from equivalent form state', () => {
46
+ // The page editor's path.
47
+ const fromRecord = auditPage(
48
+ buildPageAuditInput({ sections, meta: metaFromRecord(record), slug: 'about', record }),
49
+ )
50
+
51
+ // The SEO pane's path, with the form holding exactly what was saved.
52
+ const fromForm = auditPage(
53
+ buildPageAuditInput({
54
+ sections,
55
+ meta: {
56
+ metaTitle: 'Headless CMS for teams',
57
+ metaDescription: record.metaDescription!,
58
+ focusKeyword: 'headless cms',
59
+ structuredDataType: 'WebPage',
60
+ noindex: false,
61
+ nosnippet: false,
62
+ llmsInclude: 'AUTO',
63
+ hasOgImage: false,
64
+ },
65
+ slug: 'about',
66
+ record,
67
+ }),
68
+ )
69
+
70
+ expect(fromForm.seo.score).toBe(fromRecord.seo.score)
71
+ expect(fromForm.geo.score).toBe(fromRecord.geo.score)
72
+ })
73
+
74
+ it('measures the rendered title, not the raw field', () => {
75
+ // The site template is applied before the length check, so the advice
76
+ // matches what a searcher sees. Checking the raw field would call a title
77
+ // "too short" that renders well over the limit once the template lands.
78
+ const input = buildPageAuditInput({
79
+ sections,
80
+ meta: { ...metaFromRecord(record), metaTitle: 'Short' },
81
+ slug: 'about',
82
+ record,
83
+ })
84
+ expect(input.seo.title).toContain('Prenta')
85
+ expect(input.seo.title.length).toBeGreaterThan('Short'.length)
86
+ })
87
+
88
+ it('treats a set OG image as unknown, and an absent one as failing', () => {
89
+ // The record stores a URL with no dimensions, and the check needs width
90
+ // and ratio — so "an image exists" is not the same as "it passes".
91
+ const withImage = buildPageAuditInput({
92
+ sections,
93
+ meta: { ...metaFromRecord(record), hasOgImage: true },
94
+ slug: 'about',
95
+ record,
96
+ })
97
+ expect(withImage.seo.ogImage).toBeUndefined()
98
+
99
+ const without = buildPageAuditInput({
100
+ sections,
101
+ meta: { ...metaFromRecord(record), hasOgImage: false },
102
+ slug: 'about',
103
+ record,
104
+ })
105
+ expect(without.seo.ogImage).toBeNull()
106
+ })
107
+
108
+ it('distinguishes a removed share image from an unset one', () => {
109
+ // The share card lets an editor clear the image. An empty string is a
110
+ // deliberate removal and must fail the check; `undefined` means "an image
111
+ // exists but its dimensions are unknown here" and must not be scored.
112
+ const removed = buildPageAuditInput({
113
+ sections,
114
+ meta: { ...metaFromRecord(record), hasOgImage: false },
115
+ slug: 'about',
116
+ record,
117
+ })
118
+ expect(removed.seo.ogImage).toBeNull()
119
+ expect(auditPage(removed).seo.checks.find((c) => c.id === 'social-image')?.outcome).toBe('fail')
120
+
121
+ const chosen = buildPageAuditInput({
122
+ sections,
123
+ meta: { ...metaFromRecord(record), hasOgImage: true },
124
+ slug: 'about',
125
+ record,
126
+ })
127
+ expect(auditPage(chosen).seo.checks.find((c) => c.id === 'social-image')?.outcome).toBe(
128
+ 'not-applicable',
129
+ )
130
+ })
131
+
132
+ it('reflects an unsaved noindex immediately', () => {
133
+ // Live form state must be able to gate the score before a save round-trip.
134
+ const audit = auditPage(
135
+ buildPageAuditInput({
136
+ sections,
137
+ meta: { ...metaFromRecord(record), noindex: true },
138
+ slug: 'about',
139
+ record,
140
+ }),
141
+ )
142
+ expect(audit.seo.score).toBeNull()
143
+ })
144
+ })
@@ -169,6 +169,8 @@ describe('section type registry', () => {
169
169
  'text',
170
170
  'feature',
171
171
  'stats',
172
+ 'badges',
173
+ 'faq',
172
174
  'cta',
173
175
  'form',
174
176
  'article-body',
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { pendingSlugChange } from '../../lib/page-redirect.js'
3
+
4
+ describe('pendingSlugChange', () => {
5
+ it('reports the move when a live page changes path', () => {
6
+ expect(pendingSlugChange('/about', '/about-us')).toEqual({ from: '/about', to: '/about-us' })
7
+ })
8
+
9
+ it('stays quiet for a page that has never been live', () => {
10
+ // Nothing out there points at the old path, so there is nothing to redirect
11
+ // — and a redirect for a URL that was never public is dead weight in a
12
+ // table consulted on every request.
13
+ expect(pendingSlugChange(null, '/about-us')).toBeNull()
14
+ })
15
+
16
+ it('stays quiet when the path has not moved', () => {
17
+ expect(pendingSlugChange('/about', '/about')).toBeNull()
18
+ // Trailing whitespace from an input is not a move.
19
+ expect(pendingSlugChange('/about', ' /about ')).toBeNull()
20
+ })
21
+
22
+ it('refuses a redirect with an empty side', () => {
23
+ // A rule from or to nothing is not a redirect; posting one would put a
24
+ // broken entry in the table rather than fail loudly.
25
+ expect(pendingSlugChange('', '/about')).toBeNull()
26
+ expect(pendingSlugChange('/about', '')).toBeNull()
27
+ expect(pendingSlugChange('/about', ' ')).toBeNull()
28
+ })
29
+ })
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ RAIL_DEFAULT_WIDTH,
4
+ RAIL_MAX_WIDTH,
5
+ RAIL_MIN_WIDTH,
6
+ clampRailWidth,
7
+ readRailWidth,
8
+ writeRailWidth,
9
+ } from '../../lib/rail-width.js'
10
+
11
+ function fakeStorage(initial: Record<string, string> = {}) {
12
+ const map = new Map(Object.entries(initial))
13
+ return {
14
+ getItem: (k: string) => map.get(k) ?? null,
15
+ setItem: (k: string, v: string) => void map.set(k, v),
16
+ read: (k: string) => map.get(k) ?? null,
17
+ }
18
+ }
19
+
20
+ describe('clampRailWidth', () => {
21
+ it('holds the rail inside its usable range', () => {
22
+ expect(clampRailWidth(200)).toBe(RAIL_MIN_WIDTH)
23
+ expect(clampRailWidth(900)).toBe(RAIL_MAX_WIDTH)
24
+ expect(clampRailWidth(420)).toBe(420)
25
+ })
26
+
27
+ it('rounds to whole pixels', () => {
28
+ // Pointer deltas are fractional on scaled displays; a fractional width
29
+ // makes the rail's border land between device pixels and shimmer.
30
+ expect(clampRailWidth(420.6)).toBe(421)
31
+ })
32
+
33
+ it('falls back to the default on junk rather than producing NaN width', () => {
34
+ expect(clampRailWidth(Number.NaN)).toBe(RAIL_DEFAULT_WIDTH)
35
+ expect(clampRailWidth(Number.POSITIVE_INFINITY)).toBe(RAIL_DEFAULT_WIDTH)
36
+ })
37
+ })
38
+
39
+ describe('readRailWidth', () => {
40
+ it('returns the default when nothing is stored', () => {
41
+ expect(readRailWidth(fakeStorage())).toBe(RAIL_DEFAULT_WIDTH)
42
+ })
43
+
44
+ it('reads a stored width', () => {
45
+ expect(readRailWidth(fakeStorage({ 'pb.panelW': '480' }))).toBe(480)
46
+ })
47
+
48
+ it('clamps a stored width that is out of range', () => {
49
+ // A width persisted before the range changed, or hand-edited in devtools,
50
+ // must not be able to render an unusable rail.
51
+ expect(readRailWidth(fakeStorage({ 'pb.panelW': '5000' }))).toBe(RAIL_MAX_WIDTH)
52
+ expect(readRailWidth(fakeStorage({ 'pb.panelW': '10' }))).toBe(RAIL_MIN_WIDTH)
53
+ })
54
+
55
+ it('survives a non-numeric stored value', () => {
56
+ expect(readRailWidth(fakeStorage({ 'pb.panelW': 'wide' }))).toBe(RAIL_DEFAULT_WIDTH)
57
+ })
58
+
59
+ it('survives storage that throws', () => {
60
+ // Safari in private mode throws on getItem. A rail that cannot size itself
61
+ // must not take down the editor.
62
+ const hostile = {
63
+ getItem: () => {
64
+ throw new Error('SecurityError')
65
+ },
66
+ }
67
+ expect(readRailWidth(hostile)).toBe(RAIL_DEFAULT_WIDTH)
68
+ })
69
+ })
70
+
71
+ describe('writeRailWidth', () => {
72
+ it('persists the clamped value, not the raw one', () => {
73
+ const store = fakeStorage()
74
+ writeRailWidth(9000, store)
75
+ expect(store.read('pb.panelW')).toBe(String(RAIL_MAX_WIDTH))
76
+ })
77
+
78
+ it('swallows a storage failure', () => {
79
+ const hostile = {
80
+ setItem: () => {
81
+ throw new Error('QuotaExceededError')
82
+ },
83
+ }
84
+ expect(() => writeRailWidth(400, hostile)).not.toThrow()
85
+ })
86
+ })
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { PageSection } from '@prenta/core/page-sections'
3
+ import { baselineFieldText, snapshotBaseline } from '../../lib/reflow-baseline.js'
4
+
5
+ const sections = [
6
+ {
7
+ id: 's1',
8
+ sectionType: 'hero',
9
+ name: 'Hero',
10
+ visible: true,
11
+ content: {
12
+ heading: 'Ship content without layout drift',
13
+ body: 'A design-owned authoring surface.',
14
+ stats: [{ label: 'Uptime', value: '99.9%' }],
15
+ count: 3,
16
+ },
17
+ settings: {},
18
+ },
19
+ ] as unknown as PageSection[]
20
+
21
+ describe('reflow baseline', () => {
22
+ it('captures nothing for a page that is not live', () => {
23
+ // A draft has no published layout to break. An empty snapshot would be
24
+ // worse than none: it reads as "every field was blank", so every line of
25
+ // existing copy is reported as newly added.
26
+ expect(snapshotBaseline(sections, 'DRAFT')).toBeNull()
27
+ expect(snapshotBaseline(sections, 'SCHEDULED')).toBeNull()
28
+ expect(snapshotBaseline(sections, 'PUBLISHED')).not.toBeNull()
29
+ })
30
+
31
+ it('is a copy, so later edits to the draft do not move it', () => {
32
+ // The baseline is what is live. If it aliased the section's content object,
33
+ // editing the draft would silently drag the baseline along and the guard
34
+ // would report that nothing ever changed.
35
+ const baseline = snapshotBaseline(sections, 'PUBLISHED')
36
+ ;(sections[0]!.content as Record<string, unknown>).heading = 'Edited in the draft'
37
+
38
+ expect(baselineFieldText(baseline, 's1', 'heading')).toBe('Ship content without layout drift')
39
+ // Restore for the remaining cases.
40
+ ;(sections[0]!.content as Record<string, unknown>).heading = 'Ship content without layout drift'
41
+ })
42
+
43
+ it('resolves repeater paths the renderers emit', () => {
44
+ const baseline = snapshotBaseline(sections, 'PUBLISHED')
45
+ expect(baselineFieldText(baseline, 's1', 'stats.0.label')).toBe('Uptime')
46
+ })
47
+
48
+ it('returns undefined for anything that is not baseline text', () => {
49
+ const baseline = snapshotBaseline(sections, 'PUBLISHED')
50
+
51
+ // No snapshot at all.
52
+ expect(baselineFieldText(null, 's1', 'heading')).toBeUndefined()
53
+ // Section added since publish.
54
+ expect(baselineFieldText(baseline, 'brand-new', 'heading')).toBeUndefined()
55
+ // Field that did not exist.
56
+ expect(baselineFieldText(baseline, 's1', 'subheading')).toBeUndefined()
57
+ // Path that runs off the end of a scalar.
58
+ expect(baselineFieldText(baseline, 's1', 'heading.nope')).toBeUndefined()
59
+ // Present but not a string — a count has no wrapping to compare.
60
+ expect(baselineFieldText(baseline, 's1', 'count')).toBeUndefined()
61
+ // Index past the end of a repeater.
62
+ expect(baselineFieldText(baseline, 's1', 'stats.7.label')).toBeUndefined()
63
+ })
64
+ })
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ linesFromMetrics,
4
+ reflowSeverity,
5
+ worstBreakpoint,
6
+ type FieldLineCount,
7
+ } from '../../lib/reflow-probe.js'
8
+
9
+ describe('linesFromMetrics', () => {
10
+ it('divides height by line height', () => {
11
+ expect(linesFromMetrics(72, 24, 16)).toBe(3)
12
+ })
13
+
14
+ it('rounds rather than floors', () => {
15
+ // Sub-pixel line heights accumulate; 2.97 lines is three lines on screen,
16
+ // and flooring would under-report exactly the overflow being guarded.
17
+ expect(linesFromMetrics(71.3, 24, 16)).toBe(3)
18
+ })
19
+
20
+ it('falls back to a ratio when line-height computes to normal', () => {
21
+ // `parseFloat('normal')` is NaN — every element that never set a
22
+ // line-height would otherwise measure as one line forever.
23
+ expect(linesFromMetrics(60, Number.NaN, 16)).toBe(3)
24
+ })
25
+
26
+ it('never reports zero lines', () => {
27
+ // An empty field still occupies a line's worth of intent, and zero would
28
+ // read as "this shrank" against any baseline.
29
+ expect(linesFromMetrics(0, 24, 16)).toBe(1)
30
+ expect(linesFromMetrics(0, 0, 0)).toBe(1)
31
+ })
32
+ })
33
+
34
+ describe('reflowSeverity', () => {
35
+ it('flags one extra line as growth and two as bad', () => {
36
+ expect(reflowSeverity(3, 2)).toBe('grew')
37
+ expect(reflowSeverity(4, 2)).toBe('grew-badly')
38
+ })
39
+
40
+ it('stays quiet when copy shrinks', () => {
41
+ // Shorter copy cannot break a layout that already held the longer version.
42
+ // Flagging it would put a warning on the edit the editor was asked to make.
43
+ expect(reflowSeverity(1, 3)).toBe('same')
44
+ expect(reflowSeverity(2, 2)).toBe('same')
45
+ })
46
+ })
47
+
48
+ describe('worstBreakpoint', () => {
49
+ const at = (pairs: Array<[string, number]>): FieldLineCount[] =>
50
+ pairs.map(([id, lines]) => ({ id, lines }) as FieldLineCount)
51
+
52
+ it('picks the biggest jump', () => {
53
+ const worst = worstBreakpoint(
54
+ at([
55
+ ['wide', 3],
56
+ ['laptop', 5],
57
+ ['mobile', 4],
58
+ ]),
59
+ at([
60
+ ['wide', 3],
61
+ ['laptop', 3],
62
+ ['mobile', 3],
63
+ ]),
64
+ )
65
+ expect(worst).toBe('laptop')
66
+ })
67
+
68
+ it('breaks a tie toward the narrowest width', () => {
69
+ // The same damage at several sizes is most likely to look broken where
70
+ // there is least room.
71
+ const worst = worstBreakpoint(
72
+ at([
73
+ ['4k', 4],
74
+ ['laptop', 4],
75
+ ['mobile', 4],
76
+ ]),
77
+ at([
78
+ ['4k', 3],
79
+ ['laptop', 3],
80
+ ['mobile', 3],
81
+ ]),
82
+ )
83
+ expect(worst).toBe('mobile')
84
+ })
85
+
86
+ it('returns nothing when no breakpoint grew', () => {
87
+ expect(worstBreakpoint(at([['wide', 2]]), at([['wide', 3]]))).toBeNull()
88
+ })
89
+
90
+ it('ignores breakpoints missing from the baseline', () => {
91
+ // A baseline captured before a preset existed must not make that preset
92
+ // look like a regression.
93
+ expect(worstBreakpoint(at([['4k', 9]]), at([['wide', 1]]))).toBeNull()
94
+ })
95
+ })