@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,107 @@
1
+ /**
2
+ * Breakpoint presets for the editor canvas — shared by the page editor, the
3
+ * post editor, and the site-preview frame.
4
+ *
5
+ * Each entry's `width` is the **real CSS width** the canvas renders at. The
6
+ * canvas is then scaled down with a transform to fit the available column, so
7
+ * the page reflows against its own width via container queries rather than
8
+ * being squeezed by a max-width clamp. A clamp does not reflow: it produces a
9
+ * narrow desktop layout, not the mobile layout.
10
+ *
11
+ * `4k` is 2560, not 3840. 2560 is the CSS width a 4K/Retina display reports
12
+ * once device-pixel-ratio is applied; 3840 is the physical pixel count and no
13
+ * stylesheet ever sees it. It is also the only preset above the canvas's
14
+ * `min-width: 2000px` container step, so it is the only one that exercises
15
+ * that branch.
16
+ */
17
+
18
+ export type BreakpointId = '4k' | 'wide' | 'laptop' | 'tabletL' | 'tabletP' | 'mobile'
19
+
20
+ export type BreakpointGroup = 'DESKTOP' | 'TABLET' | 'MOBILE'
21
+
22
+ export interface Breakpoint {
23
+ id: BreakpointId
24
+ /** Full name, shown in the picker menu. */
25
+ name: string
26
+ /** Two-letter form for the collapsed trigger. */
27
+ abbr: string
28
+ /** lucide-react icon name */
29
+ icon: string
30
+ /** Real CSS width of the preview surface, in px. */
31
+ width: number
32
+ group: BreakpointGroup
33
+ }
34
+
35
+ export const BREAKPOINTS: readonly Breakpoint[] = [
36
+ { id: '4k', name: '4K / Retina', abbr: '4K', icon: 'monitor', width: 2560, group: 'DESKTOP' },
37
+ { id: 'wide', name: 'Wide Screen', abbr: 'WS', icon: 'monitor', width: 1920, group: 'DESKTOP' },
38
+ { id: 'laptop', name: 'Laptop', abbr: 'LT', icon: 'laptop', width: 1440, group: 'DESKTOP' },
39
+ {
40
+ id: 'tabletL',
41
+ name: 'Tablet Landscape',
42
+ abbr: 'TL',
43
+ icon: 'tablet',
44
+ width: 1024,
45
+ group: 'TABLET',
46
+ },
47
+ {
48
+ id: 'tabletP',
49
+ name: 'Tablet Portrait',
50
+ abbr: 'TP',
51
+ icon: 'tablet',
52
+ width: 768,
53
+ group: 'TABLET',
54
+ },
55
+ { id: 'mobile', name: 'Mobile', abbr: 'M', icon: 'smartphone', width: 390, group: 'MOBILE' },
56
+ ]
57
+
58
+ /** Menu order. Groups render as labelled sections in the breakpoint picker. */
59
+ export const BREAKPOINT_GROUPS: readonly BreakpointGroup[] = ['DESKTOP', 'TABLET', 'MOBILE']
60
+
61
+ export const DEFAULT_BREAKPOINT: BreakpointId = 'wide'
62
+
63
+ const BY_ID = new Map(BREAKPOINTS.map((b) => [b.id, b]))
64
+
65
+ export function getBreakpoint(id: BreakpointId): Breakpoint {
66
+ return BY_ID.get(id) ?? BY_ID.get(DEFAULT_BREAKPOINT)!
67
+ }
68
+
69
+ export function breakpointsInGroup(group: BreakpointGroup): Breakpoint[] {
70
+ return BREAKPOINTS.filter((b) => b.group === group)
71
+ }
72
+
73
+ /**
74
+ * Narrows arbitrary input (persisted state, a URL param, a stale prop) to a
75
+ * known id. The v0.92 preset ids — `1080p` and `tablet` — map onto their
76
+ * closest current equivalents so an editor open across the upgrade doesn't
77
+ * land on the default.
78
+ */
79
+ const LEGACY_IDS: Record<string, BreakpointId> = {
80
+ '1080p': 'laptop', // was 1440 — same width, renamed
81
+ tablet: 'tabletP', // was 768 — same width, split into landscape/portrait
82
+ }
83
+
84
+ export function coerceBreakpointId(value: unknown): BreakpointId {
85
+ if (typeof value !== 'string') return DEFAULT_BREAKPOINT
86
+ if (BY_ID.has(value as BreakpointId)) return value as BreakpointId
87
+ return LEGACY_IDS[value] ?? DEFAULT_BREAKPOINT
88
+ }
89
+
90
+ export type Zoom = 'fit' | 1
91
+
92
+ /**
93
+ * Scale factor for the canvas transform.
94
+ *
95
+ * `available` must come from a zero-height sentinel measured inside the canvas
96
+ * panel — NOT from `panel.clientWidth - padding`. The two disagree whenever the
97
+ * host scales the preview or a scrollbar appears, and the padding arithmetic is
98
+ * the kind that silently rots when the gutter changes.
99
+ *
100
+ * Never scales above 1: a 390px mobile canvas blown up to fill a 1200px column
101
+ * would show text at 3× the size the visitor sees.
102
+ */
103
+ export function fitScale(available: number, width: number, zoom: Zoom = 'fit'): number {
104
+ if (zoom !== 'fit') return 1
105
+ if (!Number.isFinite(available) || available <= 0 || width <= 0) return 1
106
+ return Math.min(1, available / width)
107
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shared edit affordances for the page/post canvas and Site preview iframe.
3
+ *
4
+ * Structure canvas: section hover is styled in `theme.css` under `.prenta-canvas`.
5
+ * Site preview: the same field chrome is injected into the iframe document when
6
+ * same-origin (CMS-host preview) so consumers get hover borders without shipping CSS.
7
+ */
8
+
9
+ /** localStorage key for the dismissible canvas tip. */
10
+ export const CANVAS_EDIT_TIP_STORAGE_KEY = 'prenta.canvasEditTip.dismissed'
11
+
12
+ /**
13
+ * CSS injected into the Site preview iframe while editing is enabled.
14
+ * Uses system colors so it works without admin theme tokens in the iframe.
15
+ */
16
+ export const PREVIEW_EDITABLE_FIELD_CHROME_CSS = `
17
+ /* Prenta Site preview — editable field hover chrome (injected by @prenta/admin) */
18
+ [data-cms-field] {
19
+ outline: 1px solid transparent;
20
+ outline-offset: 2px;
21
+ border-radius: 2px;
22
+ transition: outline-color 120ms ease;
23
+ cursor: text;
24
+ }
25
+ [data-cms-field]:hover,
26
+ [data-cms-field]:focus,
27
+ [data-cms-field]:focus-within {
28
+ outline-color: color-mix(in oklab, CanvasText 24%, transparent);
29
+ }
30
+ [data-cms-field][data-cms-field-kind="media"] {
31
+ cursor: pointer;
32
+ }
33
+ [data-cms-field][data-cms-field-kind="media"]:hover,
34
+ [data-cms-field][data-cms-field-kind="media"]:focus {
35
+ outline-color: color-mix(in oklab, CanvasText 32%, transparent);
36
+ }
37
+ @media (prefers-reduced-motion: reduce) {
38
+ [data-cms-field] {
39
+ transition: none;
40
+ }
41
+ }
42
+ `.trim()
43
+
44
+ const STYLE_ATTR = 'data-prenta-edit-chrome'
45
+
46
+ /**
47
+ * Inject (or refresh) editable-field hover chrome into a same-origin preview
48
+ * iframe. No-ops on cross-origin frames (browser blocks contentDocument).
49
+ */
50
+ export function injectPreviewEditChrome(iframe: HTMLIFrameElement | null): boolean {
51
+ try {
52
+ const doc = iframe?.contentDocument
53
+ if (!doc?.head) return false
54
+ let style = doc.head.querySelector<HTMLStyleElement>(`style[${STYLE_ATTR}]`)
55
+ if (!style) {
56
+ style = doc.createElement('style')
57
+ style.setAttribute(STYLE_ATTR, '1')
58
+ doc.head.appendChild(style)
59
+ }
60
+ style.textContent = PREVIEW_EDITABLE_FIELD_CHROME_CSS
61
+ doc.documentElement.setAttribute('data-prenta-preview-editing', 'true')
62
+ return true
63
+ } catch {
64
+ return false
65
+ }
66
+ }
67
+
68
+ export function isCanvasEditTipDismissed(): boolean {
69
+ try {
70
+ return localStorage.getItem(CANVAS_EDIT_TIP_STORAGE_KEY) === '1'
71
+ } catch {
72
+ return false
73
+ }
74
+ }
75
+
76
+ export function dismissCanvasEditTip(): void {
77
+ try {
78
+ localStorage.setItem(CANVAS_EDIT_TIP_STORAGE_KEY, '1')
79
+ } catch {
80
+ /* private mode — tip will reappear next session */
81
+ }
82
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Presentation helpers for the media block.
3
+ *
4
+ * Pure, because these are the parts with real logic — the rest of the block is
5
+ * layout. Ported from the design prototype so the labels an editor reads match
6
+ * the ones the design specifies.
7
+ */
8
+
9
+ /** The crop the template renders an image at. Drives the frame's aspect ratio. */
10
+ export type MediaCrop = 'section' | 'social'
11
+
12
+ /**
13
+ * Aspect ratio per crop, as a CSS `aspect-ratio` value.
14
+ *
15
+ * The frame shows the crop the TEMPLATE renders, not the asset's own ratio.
16
+ * Showing the asset's ratio would tell an editor their portrait photo is fine
17
+ * and then crop its subject out on the page.
18
+ */
19
+ export const CROP_RATIO: Record<MediaCrop, string> = {
20
+ section: '16 / 10',
21
+ social: '1.91 / 1',
22
+ }
23
+
24
+ /** Named ratios an editor recognises, nearest-match within a tolerance. */
25
+ const KNOWN_RATIOS: Array<[number, string]> = [
26
+ [1.91, '1.91:1'],
27
+ [16 / 9, '16:9'],
28
+ [1.5, '3:2'],
29
+ [4 / 3, '4:3'],
30
+ [1, '1:1'],
31
+ [0.8, '4:5'],
32
+ [9 / 16, '9:16'],
33
+ ]
34
+
35
+ /**
36
+ * Human ratio for an asset's intrinsic dimensions.
37
+ *
38
+ * Snaps to a named ratio when within 0.05, else reports the number. The
39
+ * tolerance matters: a 2400×1600 photo is 1.5 exactly, but a 2401×1600 scan is
40
+ * 1.5006 and an editor should still read "3:2" rather than "1.50:1".
41
+ *
42
+ * Returns null when dimensions are unknown — the library does not always have
43
+ * them, and inventing "1.00:1" would be worse than saying nothing.
44
+ */
45
+ export function ratioLabel(width?: number, height?: number): string | null {
46
+ if (!width || !height || width <= 0 || height <= 0) return null
47
+ const r = width / height
48
+ const best = KNOWN_RATIOS.reduce((a, b) => (Math.abs(b[0] - r) < Math.abs(a[0] - r) ? b : a))
49
+ return Math.abs(best[0] - r) < 0.05 ? best[1] : `${r.toFixed(2)}:1`
50
+ }
51
+
52
+ /**
53
+ * Short format badge — the file extension, uppercased.
54
+ *
55
+ * Falls back to the MIME subtype so a URL with no extension (a signed CDN link,
56
+ * an upload route) still shows something meaningful rather than a blank chip.
57
+ */
58
+ export function formatBadge(filenameOrUrl: string, mimeType?: string): string {
59
+ const path = filenameOrUrl.split(/[?#]/)[0] ?? ''
60
+ const segment = path.split('/').pop() ?? ''
61
+ // Only a segment that actually contains a dot has an extension. Comparing
62
+ // against the whole path instead of the segment let a dotless filename —
63
+ // which is what signed CDN links look like — read as its own extension.
64
+ const ext = segment.includes('.') ? (segment.split('.').pop() ?? '') : ''
65
+ if (ext && ext.length <= 5 && /^[a-z0-9]+$/i.test(ext)) {
66
+ return ext.toUpperCase()
67
+ }
68
+ const sub = mimeType?.split('/')[1]
69
+ return sub ? sub.split('+')[0]!.toUpperCase() : 'FILE'
70
+ }
71
+
72
+ /**
73
+ * Filename for display, from a filename or a URL.
74
+ *
75
+ * Query strings are dropped — a signed URL's token is noise in a 200px-wide
76
+ * toolbar, and it is never what an editor is trying to read.
77
+ */
78
+ export function displayName(filenameOrUrl: string): string {
79
+ const path = filenameOrUrl.split(/[?#]/)[0] ?? ''
80
+ return decodeURIComponent(path.split('/').pop() ?? '') || filenameOrUrl
81
+ }
82
+
83
+ /**
84
+ * A stable background tint for an asset.
85
+ *
86
+ * The design fills the frame with a wash derived from the asset's dominant
87
+ * colour. The media library stores no such value, so it is derived from the
88
+ * name instead: the same asset is always the same colour, which is the property
89
+ * that actually matters for recognising a thumbnail, and it costs no storage
90
+ * and no extra request.
91
+ *
92
+ * Returned as a 135° gradient to the design's `color-mix(tint 45%, #fff)`.
93
+ */
94
+ export function assetWash(seed: string): string {
95
+ let hash = 0
96
+ for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) % 360
97
+ const tint = `oklch(0.88 0.06 ${hash})`
98
+ return `linear-gradient(135deg, ${tint}, color-mix(in srgb, ${tint} 45%, #fff))`
99
+ }
@@ -0,0 +1,104 @@
1
+ import { resolveRenderedMetaTitle } from '@prenta/core/client'
2
+ import type { PageSection } from '@prenta/core/page-sections'
3
+ import type { PageAuditInput } from '@prenta/core/seo/page-audit'
4
+ import type { SeoContentRecord } from './seo-service.js'
5
+
6
+ /**
7
+ * One place that turns editor state into `PageAuditInput`.
8
+ *
9
+ * Two callers build the audit and their numbers must agree:
10
+ *
11
+ * - the page editor, from the SAVED record, so the header chip and rail dot
12
+ * have a score from first paint rather than after someone opens a tab;
13
+ * - the SEO pane, from LIVE form state, so the score moves as you type.
14
+ *
15
+ * They differ only in where the metadata comes from — saved versus unsaved —
16
+ * which is exactly the difference an editor expects to see. Everything else
17
+ * (which fields feed which check, how the title is rendered, what counts as
18
+ * unknown) is decided here once. Two hand-written copies of this mapping is how
19
+ * the editor ended up showing three different scores in the first place.
20
+ */
21
+ export interface AuditMetaSource {
22
+ /** Page-level meta title, before the site title template is applied. */
23
+ metaTitle: string
24
+ metaDescription: string
25
+ focusKeyword: string
26
+ structuredDataType: string
27
+ noindex: boolean
28
+ /** `nosnippet` — blocks AI Overview and chat citations outright. */
29
+ nosnippet: boolean
30
+ /** `NEVER` excludes the page from llms.txt. */
31
+ llmsInclude: string
32
+ /** True when an OG image is set — dimensions are not stored, see below. */
33
+ hasOgImage: boolean
34
+ }
35
+
36
+ export interface BuildAuditInputArgs {
37
+ sections: PageSection[]
38
+ meta: AuditMetaSource
39
+ slug: string
40
+ /** Document title and site template, for the rendered `<title>`. */
41
+ record: Pick<SeoContentRecord, 'title' | 'siteName' | 'titleTemplate' | 'url'> | null
42
+ /** Fallback title when there is no saved record yet. */
43
+ fallbackTitle?: string
44
+ }
45
+
46
+ export function buildPageAuditInput({
47
+ sections,
48
+ meta,
49
+ slug,
50
+ record,
51
+ fallbackTitle,
52
+ }: BuildAuditInputArgs): PageAuditInput {
53
+ // Measure the title a searcher actually sees — the page meta title with the
54
+ // site's template applied — not the raw field. Checking the raw field would
55
+ // give length advice that contradicts the rendered result.
56
+ const renderedTitle = record
57
+ ? resolveRenderedMetaTitle({
58
+ metaTitlePage: meta.metaTitle,
59
+ documentTitle: record.title,
60
+ site: {
61
+ siteName: record.siteName ?? undefined,
62
+ titleTemplate: record.titleTemplate ?? undefined,
63
+ },
64
+ })
65
+ : meta.metaTitle.trim() || fallbackTitle || ''
66
+
67
+ return {
68
+ sections,
69
+ seo: {
70
+ title: renderedTitle,
71
+ description: meta.metaDescription,
72
+ slug: slug || record?.url?.split('/').filter(Boolean).pop() || '',
73
+ keyword: meta.focusKeyword,
74
+ schema: meta.structuredDataType,
75
+ // `undefined` (unknown) when an image is set: the record stores a URL
76
+ // with no dimensions, and the check needs width and ratio. `null` when
77
+ // none is set, which is a genuine absence and correctly fails.
78
+ ogImage: meta.hasOgImage ? undefined : null,
79
+ robots: {
80
+ index: !meta.noindex,
81
+ snippet: !meta.nosnippet,
82
+ },
83
+ },
84
+ llmsTxt: meta.llmsInclude !== 'NEVER',
85
+ // Not resolved in the editor yet; both report not-applicable rather than
86
+ // failing an editor for a gap in the plumbing.
87
+ brand: '',
88
+ author: undefined,
89
+ }
90
+ }
91
+
92
+ /** Meta source from a saved SEO record. */
93
+ export function metaFromRecord(record: SeoContentRecord): AuditMetaSource {
94
+ return {
95
+ metaTitle: record.metaTitlePage ?? '',
96
+ metaDescription: record.metaDescription ?? '',
97
+ focusKeyword: record.focusKeyword ?? '',
98
+ structuredDataType: record.structuredDataType ?? 'WebPage',
99
+ noindex: record.noindex,
100
+ nosnippet: record.nosnippet ?? false,
101
+ llmsInclude: record.llmsInclude ?? 'AUTO',
102
+ hasOgImage: Boolean(record.ogImage),
103
+ }
104
+ }
@@ -79,6 +79,18 @@ export interface EditorPage {
79
79
  updatedAt: string | null
80
80
  scheduledAt: string | null
81
81
  scheduledUnpublishAt: string | null
82
+ /**
83
+ * Displayed byline. Read by `composePageMeta` into the page's structured
84
+ * data, so this is a real output and not decoration.
85
+ *
86
+ * Deliberately separate from the document's `createdById`, which the API
87
+ * never reassigns. That field records who created the record — provenance —
88
+ * and overwriting it to change a displayed name would trade an audit trail
89
+ * for a label.
90
+ */
91
+ authorName: string
92
+ /** CMS user behind the byline, when it is one. Empty for generic/guest. */
93
+ authorId: string
82
94
  /**
83
95
  * True when the document was authored in the visual Page Builder (it carries
84
96
  * a `_layout` tree). Such a page's `sections` are *compiled* from the layout
@@ -215,6 +227,8 @@ function rowToEditorPage(doc: DocumentApiRow): EditorPage {
215
227
  updatedAt: doc.updatedAt ?? null,
216
228
  scheduledAt: doc.scheduledAt ?? null,
217
229
  scheduledUnpublishAt: doc.scheduledUnpublishAt ?? null,
230
+ authorName: typeof data.authorName === 'string' ? data.authorName : '',
231
+ authorId: typeof data.authorId === 'string' ? data.authorId : '',
218
232
  builderOwned,
219
233
  }
220
234
  }
@@ -254,6 +268,8 @@ function toWriteBody(page: EditorPage, extra?: Record<string, unknown>): string
254
268
  sections: page.sections.map(({ unmanaged: _unmanaged, ...rest }) => rest),
255
269
  metaTitle: page.seoTitle,
256
270
  metaDescription: page.seoDescription,
271
+ authorName: page.authorName,
272
+ authorId: page.authorId,
257
273
  ...extra,
258
274
  }
259
275
  return JSON.stringify(payload)
@@ -312,6 +328,8 @@ export function emptyPage(parentPageId: string | null = null): EditorPage {
312
328
  updatedAt: null,
313
329
  scheduledAt: null,
314
330
  scheduledUnpublishAt: null,
331
+ authorName: '',
332
+ authorId: '',
315
333
  builderOwned: false,
316
334
  }
317
335
  }
@@ -0,0 +1,60 @@
1
+ import { cmsApi } from './api.js'
2
+
3
+ /**
4
+ * The 301 an editor owes the web when a published page moves.
5
+ *
6
+ * ── Why this is offered rather than assumed ───────────────────────────────
7
+ *
8
+ * Changing the slug of a page that is already live breaks every inbound link
9
+ * to it — search results, other sites, someone's bookmarks — and the CMS is the
10
+ * only party that knows the old address. Leaving the editor to remember it
11
+ * afterwards means the 404s are discovered by visitors.
12
+ *
13
+ * It is offered rather than created silently because a redirect is a public,
14
+ * durable artifact: it outlives the page, and a wrong one is harder to notice
15
+ * than a missing one. The editor sees `old → new` and decides.
16
+ */
17
+ export interface SlugChange {
18
+ from: string
19
+ to: string
20
+ }
21
+
22
+ /**
23
+ * The move worth redirecting, or `null`.
24
+ *
25
+ * `null` when the page has never been live (nothing out there points at the old
26
+ * path), when the path is unchanged, or when either side is empty — a redirect
27
+ * from or to nothing is not a redirect, and posting one would put a broken rule
28
+ * in a table that runs on every request.
29
+ */
30
+ export function pendingSlugChange(
31
+ publishedPath: string | null,
32
+ currentPath: string,
33
+ ): SlugChange | null {
34
+ const from = (publishedPath ?? '').trim()
35
+ const to = currentPath.trim()
36
+ if (!from || !to || from === to) return null
37
+ return { from, to }
38
+ }
39
+
40
+ /**
41
+ * Create the 301.
42
+ *
43
+ * Failure is reported, never swallowed. The page has already moved by the time
44
+ * this runs, so a silently-failed redirect leaves exactly the broken links the
45
+ * switch promised to prevent — and the editor would have no reason to suspect
46
+ * it.
47
+ */
48
+ export async function createPublishRedirect(change: SlugChange): Promise<{ error?: string }> {
49
+ const res = await cmsApi('/redirects', {
50
+ method: 'POST',
51
+ body: JSON.stringify({
52
+ source: change.from,
53
+ destination: change.to,
54
+ statusCode: 301,
55
+ source_kind: 'manual',
56
+ notes: 'Created automatically when the page slug changed on publish.',
57
+ }),
58
+ })
59
+ return res.error ? { error: res.error } : {}
60
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Editor rail sizing — pure logic, no DOM, so the clamping and persistence
3
+ * rules are testable without mounting a resizable panel.
4
+ */
5
+
6
+ export const RAIL_MIN_WIDTH = 300
7
+ export const RAIL_MAX_WIDTH = 620
8
+ export const RAIL_DEFAULT_WIDTH = 360
9
+
10
+ /** Width of the strip that replaces the rail when it is collapsed. */
11
+ export const RAIL_COLLAPSED_WIDTH = 38
12
+
13
+ export const RAIL_WIDTH_STORAGE_KEY = 'pb.panelW'
14
+
15
+ /** Constrain a candidate width to the rail's range, rejecting junk. */
16
+ export function clampRailWidth(width: number): number {
17
+ if (!Number.isFinite(width)) return RAIL_DEFAULT_WIDTH
18
+ return Math.min(RAIL_MAX_WIDTH, Math.max(RAIL_MIN_WIDTH, Math.round(width)))
19
+ }
20
+
21
+ /**
22
+ * Read the persisted width. Anything unreadable — no storage, a value written
23
+ * by an older build, a user editing localStorage — falls back to the default
24
+ * rather than throwing or producing a broken layout.
25
+ *
26
+ * Storage access is wrapped: Safari in private mode throws on `getItem`, and a
27
+ * rail that cannot size itself must not take down the editor.
28
+ */
29
+ export function readRailWidth(storage?: Pick<Storage, 'getItem'>): number {
30
+ const store = storage ?? safeStorage()
31
+ if (!store) return RAIL_DEFAULT_WIDTH
32
+ try {
33
+ const raw = store.getItem(RAIL_WIDTH_STORAGE_KEY)
34
+ if (raw === null) return RAIL_DEFAULT_WIDTH
35
+ const parsed = Number.parseInt(raw, 10)
36
+ return Number.isNaN(parsed) ? RAIL_DEFAULT_WIDTH : clampRailWidth(parsed)
37
+ } catch {
38
+ return RAIL_DEFAULT_WIDTH
39
+ }
40
+ }
41
+
42
+ export function writeRailWidth(width: number, storage?: Pick<Storage, 'setItem'>): void {
43
+ const store = storage ?? safeStorage()
44
+ if (!store) return
45
+ try {
46
+ store.setItem(RAIL_WIDTH_STORAGE_KEY, String(clampRailWidth(width)))
47
+ } catch {
48
+ // Quota or private-mode failure. The width still applies for this session.
49
+ }
50
+ }
51
+
52
+ function safeStorage(): Storage | null {
53
+ try {
54
+ return typeof localStorage === 'undefined' ? null : localStorage
55
+ } catch {
56
+ return null
57
+ }
58
+ }
@@ -0,0 +1,70 @@
1
+ import type { PageSection } from '@prenta/core/page-sections'
2
+
3
+ /**
4
+ * The content a page's sections had when they were last live.
5
+ *
6
+ * ── What "baseline" means here, and why ───────────────────────────────────
7
+ *
8
+ * The guard answers "will this edit change how the live page wraps?" — so the
9
+ * baseline has to be what is actually on the site, not what was last saved.
10
+ *
11
+ * This CMS has no draft/published split: a document carries one `status` and
12
+ * saving a PUBLISHED page updates the live content in place. So for a published
13
+ * page the live content is whatever the editor loaded, refreshed on every save
14
+ * and publish. For a page that has never been published there is no baseline at
15
+ * all, and the guard reports line counts without deltas — correctly, because
16
+ * there is no live layout to break.
17
+ *
18
+ * Deliberately NOT sourced from the versions API. Versions do hold full
19
+ * snapshots, but they record every update, so finding "the published one" means
20
+ * scanning history and guessing which entry was live — an extra request and a
21
+ * heuristic, to reconstruct something the editor already has in hand.
22
+ */
23
+ export interface ReflowBaseline {
24
+ byId: Map<string, Record<string, unknown>>
25
+ }
26
+
27
+ /**
28
+ * Snapshot section content, or `null` when the page is not live.
29
+ *
30
+ * `null` rather than an empty snapshot: an empty baseline would read as "every
31
+ * field was previously blank" and flag every line of existing copy as newly
32
+ * added.
33
+ */
34
+ export function snapshotBaseline(
35
+ sections: readonly PageSection[],
36
+ status: string,
37
+ ): ReflowBaseline | null {
38
+ if (status !== 'PUBLISHED') return null
39
+ return {
40
+ byId: new Map(sections.map((s) => [s.id, { ...(s.content ?? {}) }])),
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Resolve a `data-cms-field` path against a baseline.
46
+ *
47
+ * Paths are dotted and may index arrays (`stats.0.label`), matching the markers
48
+ * the renderers emit.
49
+ *
50
+ * Returns `undefined` for anything that is not baseline text — no snapshot, a
51
+ * section added since publish, a field that did not exist, or a non-string
52
+ * value. The guard reads `undefined` as "nothing to compare against" and
53
+ * reports no delta, which is the truthful answer in every one of those cases.
54
+ */
55
+ export function baselineFieldText(
56
+ baseline: ReflowBaseline | null,
57
+ sectionId: string,
58
+ fieldPath: string,
59
+ ): string | undefined {
60
+ const content = baseline?.byId.get(sectionId)
61
+ if (!content) return undefined
62
+
63
+ let cursor: unknown = content
64
+ for (const key of fieldPath.split('.')) {
65
+ if (cursor === null || typeof cursor !== 'object') return undefined
66
+ cursor = (cursor as Record<string, unknown>)[key]
67
+ }
68
+
69
+ return typeof cursor === 'string' ? cursor : undefined
70
+ }