@prenta/admin 0.92.0 → 0.93.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 (374) hide show
  1. package/CHANGELOG.md +552 -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__/styles/canvas-container-queries.test.d.ts +2 -0
  74. package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
  75. package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
  76. package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
  77. package/dist/__tests__/styles/packaging.test.d.ts +2 -0
  78. package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
  79. package/dist/__tests__/styles/packaging.test.js +31 -0
  80. package/dist/__tests__/styles/packaging.test.js.map +1 -0
  81. package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
  82. package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
  83. package/dist/__tests__/styles/tokens-drift.test.js +92 -0
  84. package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
  85. package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
  86. package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
  87. package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
  88. package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
  89. package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
  90. package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
  91. package/dist/__tests__/views/editor-top-bar.render.test.js +1 -1
  92. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  93. package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
  94. package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
  95. package/dist/__tests__/views/page-tab.render.test.js +151 -0
  96. package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
  97. package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
  98. package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
  99. package/dist/__tests__/views/section-class-parity.test.js +81 -0
  100. package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
  101. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
  102. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
  103. package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
  104. package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
  105. package/dist/components/BreakpointPicker.d.ts +18 -0
  106. package/dist/components/BreakpointPicker.d.ts.map +1 -0
  107. package/dist/components/BreakpointPicker.js +31 -0
  108. package/dist/components/BreakpointPicker.js.map +1 -0
  109. package/dist/components/CanvasEditTip.d.ts +6 -0
  110. package/dist/components/CanvasEditTip.d.ts.map +1 -0
  111. package/dist/components/CanvasEditTip.js +22 -0
  112. package/dist/components/CanvasEditTip.js.map +1 -0
  113. package/dist/components/EditorRail.d.ts +36 -0
  114. package/dist/components/EditorRail.d.ts.map +1 -0
  115. package/dist/components/EditorRail.js +104 -0
  116. package/dist/components/EditorRail.js.map +1 -0
  117. package/dist/components/EditorRailTabs.d.ts +32 -0
  118. package/dist/components/EditorRailTabs.d.ts.map +1 -0
  119. package/dist/components/EditorRailTabs.js +45 -0
  120. package/dist/components/EditorRailTabs.js.map +1 -0
  121. package/dist/components/MediaBlock.d.ts +49 -0
  122. package/dist/components/MediaBlock.d.ts.map +1 -0
  123. package/dist/components/MediaBlock.js +50 -0
  124. package/dist/components/MediaBlock.js.map +1 -0
  125. package/dist/components/ReflowMeter.d.ts +28 -0
  126. package/dist/components/ReflowMeter.d.ts.map +1 -0
  127. package/dist/components/ReflowMeter.js +42 -0
  128. package/dist/components/ReflowMeter.js.map +1 -0
  129. package/dist/components/ZoomChip.d.ts +17 -0
  130. package/dist/components/ZoomChip.d.ts.map +1 -0
  131. package/dist/components/ZoomChip.js +17 -0
  132. package/dist/components/ZoomChip.js.map +1 -0
  133. package/dist/components/breakpoint-icons.d.ts +3 -0
  134. package/dist/components/breakpoint-icons.d.ts.map +1 -0
  135. package/dist/components/breakpoint-icons.js +16 -0
  136. package/dist/components/breakpoint-icons.js.map +1 -0
  137. package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
  138. package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
  139. package/dist/components/page-tab/AuthorPicker.js +95 -0
  140. package/dist/components/page-tab/AuthorPicker.js.map +1 -0
  141. package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
  142. package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
  143. package/dist/components/page-tab/DirectiveSwitch.js +29 -0
  144. package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
  145. package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
  146. package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
  147. package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
  148. package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
  149. package/dist/components/seo/PageAuditPanel.d.ts +14 -0
  150. package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
  151. package/dist/components/seo/PageAuditPanel.js +59 -0
  152. package/dist/components/seo/PageAuditPanel.js.map +1 -0
  153. package/dist/components/seo/SeoCharCount.d.ts +19 -7
  154. package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
  155. package/dist/components/seo/SeoCharCount.js +30 -16
  156. package/dist/components/seo/SeoCharCount.js.map +1 -1
  157. package/dist/components/seo/SeoEditorPane.d.ts +15 -1
  158. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  159. package/dist/components/seo/SeoEditorPane.js +118 -21
  160. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  161. package/dist/components/seo/SerpPreview.d.ts +20 -0
  162. package/dist/components/seo/SerpPreview.d.ts.map +1 -0
  163. package/dist/components/seo/SerpPreview.js +6 -0
  164. package/dist/components/seo/SerpPreview.js.map +1 -0
  165. package/dist/hooks/usePageAudit.d.ts +41 -0
  166. package/dist/hooks/usePageAudit.d.ts.map +1 -0
  167. package/dist/hooks/usePageAudit.js +68 -0
  168. package/dist/hooks/usePageAudit.js.map +1 -0
  169. package/dist/hooks/useReflowGuard.d.ts +47 -0
  170. package/dist/hooks/useReflowGuard.d.ts.map +1 -0
  171. package/dist/hooks/useReflowGuard.js +52 -0
  172. package/dist/hooks/useReflowGuard.js.map +1 -0
  173. package/dist/lib/breakpoints.d.ts +51 -0
  174. package/dist/lib/breakpoints.d.ts.map +1 -0
  175. package/dist/lib/breakpoints.js +84 -0
  176. package/dist/lib/breakpoints.js.map +1 -0
  177. package/dist/lib/canvas-edit-chrome.d.ts +22 -0
  178. package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
  179. package/dist/lib/canvas-edit-chrome.js +81 -0
  180. package/dist/lib/canvas-edit-chrome.js.map +1 -0
  181. package/dist/lib/media-format.d.ts +55 -0
  182. package/dist/lib/media-format.d.ts.map +1 -0
  183. package/dist/lib/media-format.js +93 -0
  184. package/dist/lib/media-format.js.map +1 -0
  185. package/dist/lib/page-audit-input.d.ts +45 -0
  186. package/dist/lib/page-audit-input.d.ts.map +1 -0
  187. package/dist/lib/page-audit-input.js +53 -0
  188. package/dist/lib/page-audit-input.js.map +1 -0
  189. package/dist/lib/page-editor-service.d.ts +12 -0
  190. package/dist/lib/page-editor-service.d.ts.map +1 -1
  191. package/dist/lib/page-editor-service.js +6 -0
  192. package/dist/lib/page-editor-service.js.map +1 -1
  193. package/dist/lib/page-redirect.d.ts +39 -0
  194. package/dist/lib/page-redirect.d.ts.map +1 -0
  195. package/dist/lib/page-redirect.js +38 -0
  196. package/dist/lib/page-redirect.js.map +1 -0
  197. package/dist/lib/rail-width.d.ts +23 -0
  198. package/dist/lib/rail-width.d.ts.map +1 -0
  199. package/dist/lib/rail-width.js +59 -0
  200. package/dist/lib/rail-width.js.map +1 -0
  201. package/dist/lib/reflow-baseline.d.ts +45 -0
  202. package/dist/lib/reflow-baseline.d.ts.map +1 -0
  203. package/dist/lib/reflow-baseline.js +38 -0
  204. package/dist/lib/reflow-baseline.js.map +1 -0
  205. package/dist/lib/reflow-probe.d.ts +159 -0
  206. package/dist/lib/reflow-probe.d.ts.map +1 -0
  207. package/dist/lib/reflow-probe.js +228 -0
  208. package/dist/lib/reflow-probe.js.map +1 -0
  209. package/dist/lib/seo-service.d.ts +10 -0
  210. package/dist/lib/seo-service.d.ts.map +1 -1
  211. package/dist/lib/seo-service.js.map +1 -1
  212. package/dist/prenta-admin.css +1 -1
  213. package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
  214. package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
  215. package/dist/views/page-editor/CanvasMetaLine.js +23 -0
  216. package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
  217. package/dist/views/page-editor/EditorCanvas.d.ts +34 -8
  218. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  219. package/dist/views/page-editor/EditorCanvas.js +68 -34
  220. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  221. package/dist/views/page-editor/EditorTopBar.d.ts +14 -1
  222. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  223. package/dist/views/page-editor/EditorTopBar.js +8 -17
  224. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  225. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  226. package/dist/views/page-editor/PageSectionEditor.js +275 -53
  227. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  228. package/dist/views/page-editor/PageTab.d.ts +36 -0
  229. package/dist/views/page-editor/PageTab.d.ts.map +1 -0
  230. package/dist/views/page-editor/PageTab.js +65 -0
  231. package/dist/views/page-editor/PageTab.js.map +1 -0
  232. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  233. package/dist/views/page-editor/SectionInspector.js +63 -19
  234. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  235. package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
  236. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
  237. package/dist/views/page-editor/SectionsPanel.js +38 -7
  238. package/dist/views/page-editor/SectionsPanel.js.map +1 -1
  239. package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
  240. package/dist/views/page-editor/SitePreviewFrame.js +3 -0
  241. package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
  242. package/dist/views/page-editor/reflow-context.d.ts +28 -0
  243. package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
  244. package/dist/views/page-editor/reflow-context.js +14 -0
  245. package/dist/views/page-editor/reflow-context.js.map +1 -0
  246. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
  247. package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
  248. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
  249. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
  250. package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
  251. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
  252. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
  253. package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
  254. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
  255. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
  256. package/dist/views/page-editor/sections/GallerySection.js +7 -3
  257. package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
  258. package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
  259. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
  260. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
  261. package/dist/views/page-editor/sections/MissionSection.js +1 -1
  262. package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
  263. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
  264. package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
  265. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
  266. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
  267. package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
  268. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
  269. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  270. package/dist/views/page-editor/sections/SectionRenderer.js +49 -24
  271. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  272. package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
  273. package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
  274. package/dist/views/page-editor/sections/admin-renderers.js +27 -15
  275. package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
  276. package/dist/views/page-editor/sections/parts.d.ts +0 -1
  277. package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
  278. package/dist/views/page-editor/sections/parts.js +41 -16
  279. package/dist/views/page-editor/sections/parts.js.map +1 -1
  280. package/dist/views/page-editor/viewports.d.ts +7 -14
  281. package/dist/views/page-editor/viewports.d.ts.map +1 -1
  282. package/dist/views/page-editor/viewports.js +7 -12
  283. package/dist/views/page-editor/viewports.js.map +1 -1
  284. package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
  285. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  286. package/dist/views/post-editor/PostEditorCanvas.js +5 -2
  287. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  288. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  289. package/dist/views/post-editor/PostSectionEditor.js +2 -20
  290. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  291. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
  292. package/dist/views/post-editor/PostTemplateEditor.js +4 -1
  293. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
  294. package/package.json +5 -3
  295. package/src/AdminRoot.tsx +27 -6
  296. package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
  297. package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
  298. package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
  299. package/src/__tests__/components/media-block.render.test.tsx +126 -0
  300. package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
  301. package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
  302. package/src/__tests__/components/seo-char-count.test.tsx +26 -6
  303. package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
  304. package/src/__tests__/lib/breakpoints.test.ts +84 -0
  305. package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
  306. package/src/__tests__/lib/media-format.test.ts +89 -0
  307. package/src/__tests__/lib/page-audit-input.test.ts +144 -0
  308. package/src/__tests__/lib/page-editor-service.test.ts +2 -0
  309. package/src/__tests__/lib/page-redirect.test.ts +29 -0
  310. package/src/__tests__/lib/rail-width.test.ts +86 -0
  311. package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
  312. package/src/__tests__/lib/reflow-probe.test.ts +95 -0
  313. package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
  314. package/src/__tests__/styles/packaging.test.ts +38 -0
  315. package/src/__tests__/styles/tokens-drift.test.ts +110 -0
  316. package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
  317. package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
  318. package/src/__tests__/views/editor-top-bar.render.test.tsx +1 -1
  319. package/src/__tests__/views/page-tab.render.test.tsx +226 -0
  320. package/src/__tests__/views/section-class-parity.test.ts +98 -0
  321. package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
  322. package/src/components/BreakpointPicker.tsx +114 -0
  323. package/src/components/CanvasEditTip.tsx +46 -0
  324. package/src/components/EditorRail.tsx +219 -0
  325. package/src/components/EditorRailTabs.tsx +117 -0
  326. package/src/components/MediaBlock.tsx +223 -0
  327. package/src/components/ReflowMeter.tsx +125 -0
  328. package/src/components/ZoomChip.tsx +39 -0
  329. package/src/components/breakpoint-icons.ts +17 -0
  330. package/src/components/page-tab/AuthorPicker.tsx +240 -0
  331. package/src/components/page-tab/DirectiveSwitch.tsx +86 -0
  332. package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
  333. package/src/components/seo/PageAuditPanel.tsx +192 -0
  334. package/src/components/seo/SeoCharCount.tsx +55 -24
  335. package/src/components/seo/SeoEditorPane.tsx +213 -31
  336. package/src/components/seo/SerpPreview.tsx +41 -0
  337. package/src/hooks/usePageAudit.ts +98 -0
  338. package/src/hooks/useReflowGuard.ts +96 -0
  339. package/src/lib/breakpoints.ts +107 -0
  340. package/src/lib/canvas-edit-chrome.ts +82 -0
  341. package/src/lib/media-format.ts +99 -0
  342. package/src/lib/page-audit-input.ts +104 -0
  343. package/src/lib/page-editor-service.ts +18 -0
  344. package/src/lib/page-redirect.ts +60 -0
  345. package/src/lib/rail-width.ts +58 -0
  346. package/src/lib/reflow-baseline.ts +70 -0
  347. package/src/lib/reflow-probe.ts +299 -0
  348. package/src/lib/seo-service.ts +10 -0
  349. package/src/styles/prenta-tokens.css +120 -42
  350. package/src/styles/theme.css +130 -0
  351. package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
  352. package/src/views/page-editor/EditorCanvas.tsx +109 -50
  353. package/src/views/page-editor/EditorTopBar.tsx +35 -43
  354. package/src/views/page-editor/PageSectionEditor.tsx +419 -148
  355. package/src/views/page-editor/PageTab.tsx +433 -0
  356. package/src/views/page-editor/SectionInspector.tsx +82 -41
  357. package/src/views/page-editor/SectionsPanel.tsx +281 -140
  358. package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
  359. package/src/views/page-editor/reflow-context.ts +43 -0
  360. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
  361. package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
  362. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
  363. package/src/views/page-editor/sections/GallerySection.tsx +7 -3
  364. package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
  365. package/src/views/page-editor/sections/MissionSection.tsx +4 -2
  366. package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
  367. package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
  368. package/src/views/page-editor/sections/SectionRenderer.tsx +69 -29
  369. package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
  370. package/src/views/page-editor/sections/parts.tsx +46 -18
  371. package/src/views/page-editor/viewports.ts +13 -26
  372. package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
  373. package/src/views/post-editor/PostSectionEditor.tsx +1 -56
  374. package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
@@ -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
+ })
@@ -0,0 +1,103 @@
1
+ import { readFileSync, readdirSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ /**
6
+ * The editor canvas renders a page at a device's real CSS width and scales it
7
+ * with a transform, so the page must reflow against the CANVAS width, not the
8
+ * browser viewport. Two invariants make that work, and both fail silently:
9
+ *
10
+ * 1. `.prenta-canvas` must establish an inline-size query container. Without
11
+ * it, `@pv-*` variants never match and every breakpoint renders the
12
+ * narrowest layout — which looks plausible enough to ship.
13
+ * 2. Section renderers must not use viewport variants (`sm:`, `md:`, `lg:`).
14
+ * Those resolve against the editor window, so picking "Mobile" on a wide
15
+ * monitor yields a 390px canvas still laid out for desktop — the exact
16
+ * bug that made the old preview decorative.
17
+ */
18
+
19
+ const SECTIONS_DIR = fileURLToPath(new URL('../../views/page-editor/sections/', import.meta.url))
20
+ const THEME_CSS = fileURLToPath(new URL('../../styles/theme.css', import.meta.url))
21
+
22
+ /** Viewport variants in a className position. Matches `sm:`…`2xl:` prefixes. */
23
+ const VIEWPORT_VARIANT = /(?<![\w@[-])(?:sm|md|lg|xl|2xl):[a-z0-9[]/g
24
+
25
+ function sectionFiles(): string[] {
26
+ return readdirSync(SECTIONS_DIR).filter((f) => f.endsWith('.tsx') || f.endsWith('.ts'))
27
+ }
28
+
29
+ describe('canvas container queries', () => {
30
+ it('makes .prenta-canvas an inline-size query container', () => {
31
+ const css = readFileSync(THEME_CSS, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
32
+ expect(css).toMatch(/\.prenta-canvas\s*\{[^}]*container-type:\s*inline-size/)
33
+ })
34
+
35
+ it('defines every reflow step the handoff specifies', () => {
36
+ const css = readFileSync(THEME_CSS, 'utf8')
37
+ for (const [name, value] of [
38
+ ['--container-pv-sm', '460px'],
39
+ ['--container-pv-md', '700px'],
40
+ ['--container-pv-lg', '900px'],
41
+ ['--container-pv-xl', '1100px'],
42
+ ['--container-pv-2xl', '2000px'],
43
+ ]) {
44
+ expect(css, `${name} is missing or has moved`).toContain(`${name}: ${value}`)
45
+ }
46
+ })
47
+
48
+ it('uses no viewport breakpoints in any section renderer', () => {
49
+ const offenders: string[] = []
50
+ for (const file of sectionFiles()) {
51
+ const source = readFileSync(SECTIONS_DIR + file, 'utf8')
52
+ for (const match of source.matchAll(VIEWPORT_VARIANT)) {
53
+ offenders.push(`${file}: ${match[0]}`)
54
+ }
55
+ }
56
+ expect(
57
+ offenders,
58
+ 'Section renderers must reflow on the canvas width. Use the @pv-* container ' +
59
+ 'steps (theme.css) instead of sm:/md:/lg:.',
60
+ ).toEqual([])
61
+ })
62
+
63
+ it('restores the stock type scale inside the canvas, in the utilities layer', () => {
64
+ // The admin remaps six Tailwind size keys onto its own tokens (text-base =
65
+ // 13px). The canvas renders the consumer's website, where text-base must
66
+ // be 16px — and the reflow guard measures wrapping against these sizes, so
67
+ // a canvas on the admin scale makes every line count wrong.
68
+ const css = readFileSync(THEME_CSS, 'utf8')
69
+
70
+ // Layer order beats specificity: in @layer base this override would lose
71
+ // to the very utility it corrects.
72
+ const utilities = css.slice(css.indexOf('@layer utilities'))
73
+ expect(utilities, 'canvas type restoration must be in @layer utilities').toContain(
74
+ '.prenta-admin .prenta-canvas .text-base',
75
+ )
76
+
77
+ for (const [utility, size] of [
78
+ ['text-xs', '0.75rem'],
79
+ ['text-sm', '0.875rem'],
80
+ ['text-base', '1rem'],
81
+ ['text-lg', '1.125rem'],
82
+ ['text-xl', '1.25rem'],
83
+ ['text-2xl', '1.5rem'],
84
+ ]) {
85
+ const rule = new RegExp(
86
+ `\\.prenta-canvas \\.${utility}\\s*\\{[^}]*font-size:\\s*${size!.replace('.', '\\.')}`,
87
+ )
88
+ expect(utilities, `${utility} is not restored to its stock size`).toMatch(rule)
89
+ }
90
+ })
91
+
92
+ it('only references container steps that exist', () => {
93
+ const declared = new Set(['@pv-sm', '@pv-md', '@pv-lg', '@pv-xl', '@pv-2xl'])
94
+ const used = new Set<string>()
95
+ for (const file of sectionFiles()) {
96
+ const source = readFileSync(SECTIONS_DIR + file, 'utf8')
97
+ for (const match of source.matchAll(/@pv-[\w-]+?(?=:)/g)) used.add(match[0])
98
+ }
99
+ // A typo'd step (`@pv-xxl:`) compiles to nothing and silently never applies.
100
+ expect([...used].filter((u) => !declared.has(u))).toEqual([])
101
+ expect(used.size).toBeGreaterThan(0)
102
+ })
103
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import pkg from '../../../package.json' with { type: 'json' }
3
+
4
+ /**
5
+ * `@prenta/admin` exports a precompiled stylesheet
6
+ * (`@prenta/admin/styles/precompiled.css`) that consumers import purely for its
7
+ * side effects. `sideEffects: false` tells a bundler such an import is safe to
8
+ * eliminate — the admin would then render unstyled, with no error anywhere.
9
+ *
10
+ * The field is currently ABSENT, which is the safe default: bundlers assume
11
+ * side effects. This test exists so it stays safe. `@prenta/sections-react` hit
12
+ * exactly this problem and had to be corrected before publish.
13
+ *
14
+ * If someone does add the field to enable tree-shaking, it must be an array
15
+ * that keeps CSS side-effectful, not `false`.
16
+ */
17
+ describe('@prenta/admin packaging', () => {
18
+ it('never declares itself free of side effects', () => {
19
+ const sideEffects = (pkg as { sideEffects?: unknown }).sideEffects
20
+ expect(
21
+ sideEffects,
22
+ 'sideEffects: false lets bundlers drop the precompiled CSS import',
23
+ ).not.toBe(false)
24
+ })
25
+
26
+ it('keeps CSS side-effectful if the field is ever narrowed to an array', () => {
27
+ const sideEffects = (pkg as { sideEffects?: unknown }).sideEffects
28
+ if (!Array.isArray(sideEffects)) return
29
+ expect(
30
+ sideEffects.some((p) => String(p).endsWith('.css')),
31
+ 'an array form must still list CSS',
32
+ ).toBe(true)
33
+ })
34
+
35
+ it('still exports the stylesheet consumers import', () => {
36
+ expect(pkg.exports['./styles/precompiled.css']).toBe('./dist/prenta-admin.css')
37
+ })
38
+ })
@@ -0,0 +1,110 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { describe, expect, it } from 'vitest'
4
+ // @ts-expect-error — plain .mjs build script, intentionally untyped
5
+ import { OUTPUTS, buildAll, readTokens } from '../../../scripts/generate-tokens.mjs'
6
+
7
+ /**
8
+ * The token files are generated from design-system/tokens.json. Before the
9
+ * generator existed, the runtime copy (src/styles/prenta-tokens.css) was a
10
+ * hand transcription of the handoff copy (design-system/tokens.css) and the
11
+ * two silently diverged — the radius scale was renamed in one and not the
12
+ * other, and the WCAG correction to `--muted` lived only downstream where a
13
+ * re-import of the handoff would have reverted it without anyone noticing.
14
+ *
15
+ * This suite fails if a generated file is edited by hand, if tokens.json
16
+ * changes without regenerating, or if the two CSS copies stop agreeing.
17
+ */
18
+
19
+ const PKG = fileURLToPath(new URL('../../../', import.meta.url))
20
+
21
+ const read = (rel: string) => readFileSync(PKG + rel, 'utf8')
22
+
23
+ /** Declarations only — comments legitimately differ between the two copies. */
24
+ function declarationMap(css: string): Map<string, string> {
25
+ const body = css.replace(/\/\*[\s\S]*?\*\//g, '')
26
+ const out = new Map<string, string>()
27
+ for (const match of body.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)[;}]/g)) {
28
+ const name = match[1]!
29
+ const value = match[2]!
30
+ // Later declarations win in CSS; the dark block legitimately re-declares
31
+ // names from the light block, so keep the first (light) binding per file
32
+ // section by only recording a name once per call.
33
+ if (!out.has(name)) out.set(name, value.trim())
34
+ }
35
+ return out
36
+ }
37
+
38
+ describe('design token generation', () => {
39
+ it('every generated file matches a fresh run of the generator', async () => {
40
+ const built = await buildAll(await readTokens())
41
+
42
+ for (const rel of Object.values(OUTPUTS) as string[]) {
43
+ const committed = read(rel)
44
+ expect(built[rel], `${rel} is out of date. Run: pnpm --filter @prenta/admin generate`).toBe(
45
+ committed,
46
+ )
47
+ }
48
+ })
49
+
50
+ it('marks generated files so they are not hand-edited', () => {
51
+ for (const rel of Object.values(OUTPUTS) as string[]) {
52
+ expect(read(rel), `${rel} is missing its generated-file banner`).toContain(
53
+ 'GENERATED FILE — DO NOT EDIT',
54
+ )
55
+ }
56
+ })
57
+
58
+ it('the handoff copy and the runtime copy carry identical values', () => {
59
+ const handoff = declarationMap(read(OUTPUTS.tokensCss))
60
+ const runtime = declarationMap(read(OUTPUTS.runtimeCss))
61
+
62
+ expect(runtime.size).toBeGreaterThan(60)
63
+ expect([...handoff.keys()].sort()).toEqual([...runtime.keys()].sort())
64
+ for (const [name, value] of handoff) {
65
+ expect(runtime.get(name), `${name} differs between the two token copies`).toBe(value)
66
+ }
67
+ })
68
+
69
+ it('scopes the runtime copy to .prenta-admin and never to :root', () => {
70
+ const runtime = read(OUTPUTS.runtimeCss).replace(/\/\*[\s\S]*?\*\//g, '')
71
+ expect(runtime).toContain('.prenta-admin {')
72
+ expect(runtime).not.toMatch(/(^|\s):root\s*[,{]/)
73
+ })
74
+
75
+ it('keeps deprecated aliases resolving to their canonical token', () => {
76
+ const runtime = declarationMap(read(OUTPUTS.runtimeCss))
77
+ // `--r-control` and `--motion-*` are the pre-2.0 names. 72 call sites still
78
+ // read `--motion-fast`; the aliases are what make the rename non-breaking.
79
+ expect(runtime.get('--r-control')).toBe('var(--r-ctl)')
80
+ expect(runtime.get('--motion-fast')).toBe('var(--mo-fast)')
81
+ expect(runtime.get('--motion-base')).toBe('var(--mo-base)')
82
+ })
83
+
84
+ it('keeps --muted above the WCAG AA threshold in both themes', () => {
85
+ // The handoff specifies #B0ADBE (2.2:1 on white). The admin renders --muted
86
+ // as real text, so the generator ships a corrected value recorded in
87
+ // tokens.json as a `designValue` deviation. If someone re-imports the
88
+ // handoff verbatim, this fails rather than shipping illegible hints.
89
+ const css = read(OUTPUTS.runtimeCss)
90
+ const [light, dark] = css.split('.prenta-admin[data-theme=')
91
+ const mutedOf = (block: string) => declarationMap(block).get('--muted')
92
+
93
+ expect(contrast(mutedOf(light!)!, '#ffffff')).toBeGreaterThanOrEqual(4.5)
94
+ expect(contrast(mutedOf(dark!)!, '#1c1a2e')).toBeGreaterThanOrEqual(4.5)
95
+ })
96
+ })
97
+
98
+ /** WCAG 2.1 relative-luminance contrast ratio between two hex colors. */
99
+ function contrast(a: string, b: string): number {
100
+ const lum = (hex: string) => {
101
+ const n = hex.trim().replace('#', '')
102
+ const channels = [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16) / 255)
103
+ const [r, g, bl] = channels.map((c) =>
104
+ c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4,
105
+ )
106
+ return 0.2126 * r! + 0.7152 * g! + 0.0722 * bl!
107
+ }
108
+ const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x)
109
+ return (hi! + 0.05) / (lo! + 0.05)
110
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { DEFAULT_SECTION_RENDERERS } from '@prenta/sections-react'
3
+ import type { AdminSectionRendererMap } from '../../views/page-editor/sections/admin-renderers.js'
4
+ import { ADMIN_RENDER_OPTIONS } from '../../views/page-editor/sections/admin-renderers.js'
5
+
6
+ /**
7
+ * `AdminSectionRendererMap` is a hand-maintained structural mirror of
8
+ * `SectionRendererMap` from @prenta/sections-react — cms-admin depends on
9
+ * cms-core only, so the type is duplicated rather than imported.
10
+ *
11
+ * The mirror exists so a consumer can hand `<AdminRoot sectionRenderers>` the
12
+ * same components it registers for the public site and have the editor canvas
13
+ * preview what actually ships. That only works while the two shapes stay
14
+ * assignable, and nothing else in the build checks it — the duplication is
15
+ * invisible to the compiler until someone tries the assignment.
16
+ *
17
+ * This file IS that assignment. If the shapes drift, the type-check fails here
18
+ * rather than in a consumer's project.
19
+ */
20
+
21
+ describe('admin renderer contract', () => {
22
+ it('accepts a real @prenta/sections-react renderer map', () => {
23
+ // The assignment is the assertion — this line fails `tsc` if the mirror
24
+ // drifts. Renderers are function types, so their parameters compare
25
+ // contravariantly: widening a field on the ADMIN side is what breaks it.
26
+ const supplied: AdminSectionRendererMap = DEFAULT_SECTION_RENDERERS
27
+
28
+ expect(Object.keys(supplied)).toContain('hero')
29
+ expect(typeof supplied.hero).toBe('function')
30
+ })
31
+
32
+ it('supplies every option a public renderer resolves', () => {
33
+ // sections-react's `ResolvedRenderOptions` — a missing key throws at
34
+ // render time, not compile time, because renderers destructure it.
35
+ for (const key of ['sanitizeHtml', 'baseUrl', 'renderImage', 'lcp', 'prefetchedForms']) {
36
+ expect(ADMIN_RENDER_OPTIONS, `ADMIN_RENDER_OPTIONS is missing ${key}`).toHaveProperty(key)
37
+ }
38
+ })
39
+
40
+ it('never grants the LCP priority slot in the canvas', () => {
41
+ // On a public page exactly one image loads eagerly. The canvas has no
42
+ // such budget to protect and nothing competing for bandwidth, so every
43
+ // image should lazy-load — `claim()` always declines.
44
+ expect(ADMIN_RENDER_OPTIONS.lcp.claim()).toBe(false)
45
+ expect(ADMIN_RENDER_OPTIONS.lcp.claim()).toBe(false)
46
+ expect(ADMIN_RENDER_OPTIONS.lcp.claimed).toBe(true)
47
+ })
48
+ })
@@ -84,9 +84,20 @@ describe('pluggable admin section renderers', () => {
84
84
  })
85
85
  })
86
86
 
87
- it('does not affect built-in section types', () => {
87
+ it('overrides built-in section types too', () => {
88
+ // REVERSED from the original behaviour, which consulted the map only for
89
+ // types the built-in switch did not know.
90
+ //
91
+ // The point of `<AdminRoot sectionRenderers>` is to hand the admin the
92
+ // same components the public site registers, so the canvas previews what
93
+ // actually ships. Excluding the built-ins made that impossible for the
94
+ // twelve types that matter most: a consumer passing
95
+ // `DEFAULT_SECTION_RENDERERS` from `@prenta/sections-react` had their
96
+ // hero, text, feature, stats and cta silently dropped, and the canvas
97
+ // rendered the admin's own approximations — different font size, weight
98
+ // and breakpoints — while presenting itself as a preview.
88
99
  const renderers: AdminSectionRendererMap = {
89
- hero: () => <div data-testid="hijacked-hero" />,
100
+ hero: () => <div data-testid="supplied-hero" />,
90
101
  }
91
102
  render(
92
103
  <AdminSectionRenderersProvider value={renderers}>
@@ -95,7 +106,62 @@ describe('pluggable admin section renderers', () => {
95
106
  />
96
107
  </AdminSectionRenderersProvider>,
97
108
  )
98
- // Built-in types always use the built-in canvas component.
99
- expect(screen.queryByTestId('hijacked-hero')).toBeNull()
109
+ expect(screen.getByTestId('supplied-hero')).toBeTruthy()
110
+ })
111
+
112
+ it('falls back to the built-in when a supplied renderer for a built-in type throws', () => {
113
+ // A broken consumer renderer must degrade to the admin's own component,
114
+ // not to the "custom section" placeholder — the type IS known here.
115
+ vi.spyOn(console, 'error').mockImplementation(() => {})
116
+ const renderers: AdminSectionRendererMap = {
117
+ hero: () => {
118
+ throw new Error('consumer hero exploded')
119
+ },
120
+ }
121
+ render(
122
+ <AdminSectionRenderersProvider value={renderers}>
123
+ <SectionRenderer
124
+ section={customSection({
125
+ sectionType: 'hero',
126
+ name: 'Hero',
127
+ content: { heading: 'Built-in hero copy' },
128
+ })}
129
+ />
130
+ </AdminSectionRenderersProvider>,
131
+ )
132
+ expect(screen.getByText('Built-in hero copy')).toBeTruthy()
133
+ vi.restoreAllMocks()
134
+ })
135
+
136
+ it('supplies the render options a public renderer expects', () => {
137
+ // sections-react renderers call `options.lcp.claim()` during render (hero,
138
+ // feature, gallery). A missing controller would throw on the first image,
139
+ // so the mirror must carry the whole shape, not just renderImage.
140
+ let seen: Record<string, unknown> | null = null
141
+ const renderers: AdminSectionRendererMap = {
142
+ hero: ({ options }) => {
143
+ seen = options as unknown as Record<string, unknown>
144
+ return <div />
145
+ },
146
+ }
147
+ render(
148
+ <AdminSectionRenderersProvider value={renderers}>
149
+ <SectionRenderer section={customSection({ sectionType: 'hero', content: {} })} />
150
+ </AdminSectionRenderersProvider>,
151
+ )
152
+ const options = seen as unknown as {
153
+ lcp: { claim: () => boolean }
154
+ prefetchedForms: Record<string, unknown>
155
+ renderImage: unknown
156
+ sanitizeHtml: unknown
157
+ baseUrl: string
158
+ }
159
+ expect(typeof options.lcp.claim).toBe('function')
160
+ // The canvas never grants the priority slot — nothing competes for
161
+ // bandwidth there, so every image should lazy-load.
162
+ expect(options.lcp.claim()).toBe(false)
163
+ expect(options.prefetchedForms).toEqual({})
164
+ expect(typeof options.renderImage).toBe('function')
165
+ expect(typeof options.sanitizeHtml).toBe('function')
100
166
  })
101
167
  })
@@ -12,7 +12,7 @@ const baseProps = {
12
12
  publishing: false,
13
13
  canEdit: true,
14
14
  canPublish: true,
15
- viewport: '1080p' as const,
15
+ viewport: 'wide' as const,
16
16
  onViewport: vi.fn(),
17
17
  onBack: vi.fn(),
18
18
  onPreview: vi.fn(),