@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
@@ -15,7 +15,7 @@ import {
15
15
  type ReactElement,
16
16
  type ReactNode,
17
17
  } from 'react'
18
- import { Loader2, Monitor, Smartphone, Sparkles, Wand2 } from 'lucide-react'
18
+ import { ImagePlus, Loader2, Monitor, Smartphone, Sparkles, Trash2, Wand2 } from 'lucide-react'
19
19
  import { toast } from 'sonner'
20
20
  import type { BrandAlignmentScoreResult } from '@prenta/core/brand-voice'
21
21
  import { resolveRenderedMetaTitle } from '@prenta/core/client'
@@ -25,6 +25,9 @@ import {
25
25
  stripHtmlTags,
26
26
  type SEOAnalysisResult,
27
27
  } from '@prenta/core/seo/analysis'
28
+ import { auditPage, type PageAuditResult } from '@prenta/core/seo/page-audit'
29
+ import { buildPageAuditInput } from '../../lib/page-audit-input.js'
30
+ import type { PageSection } from '@prenta/core/page-sections'
28
31
  import {
29
32
  fetchSeoContentRecord,
30
33
  updateSeoFields,
@@ -39,13 +42,21 @@ import { fixBrandVoiceContent } from '../../lib/brand-voice-client.js'
39
42
  import { BrandVoiceAlignmentBadge } from '../BrandVoiceAlignmentBadge.js'
40
43
  import { InspectorPaneBody, InspectorPaneFooter, InspectorPaneHeader } from '../ui/InspectorPane.js'
41
44
  import { Toggle } from '../ui/Toggle.js'
45
+ import { MediaPickerModal } from '../MediaPickerModal.js'
46
+ import { PageAuditPanel } from './PageAuditPanel.js'
47
+ import { SeoCharCount } from './SeoCharCount.js'
48
+ import { SerpPreview } from './SerpPreview.js'
42
49
  import { SeoLoading, SeoErrorState, gradeMeta } from './primitives.js'
43
50
  import { SeoAccordion, SeoAccordionBadge, SeoAccordionItem } from './SeoAccordion.js'
44
51
  import { SeoAnalysisList } from './SeoAnalysisList.js'
45
52
  import { SeoInsightsMetrics } from './SeoInsightsMetrics.js'
46
53
  import { SeoLinkingSection } from './SeoLinkingSection.js'
47
54
 
55
+ // Ranges the audit's title/description checks score against — one definition,
56
+ // so the meter cannot tell an editor they are in range while the check fails.
57
+ const META_TITLE_MIN = 30
48
58
  const META_TITLE_MAX = 60
59
+ const META_DESC_MIN = 70
49
60
  const META_DESC_MAX = 160
50
61
  const KEY_TAKEAWAY_MAX = 240
51
62
  const LLMS_DESC_MAX = 160
@@ -66,15 +77,6 @@ function seoBrandChannel(field: AiField): string | undefined {
66
77
  const inputCls =
67
78
  'w-full rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] px-2.5 py-1.5 text-[12.5px] text-[var(--txt)] placeholder:text-[var(--muted)] focus:border-[var(--acc)] focus:outline-none'
68
79
 
69
- function CharCount({ value, max }: { value: string; max: number }) {
70
- const over = value.length > max
71
- return (
72
- <span className={`text-[11px] ${over ? 'text-[var(--err)]' : 'text-[var(--muted)]'}`}>
73
- {value.length}/{max}
74
- </span>
75
- )
76
- }
77
-
78
80
  function PaneField({
79
81
  label,
80
82
  children,
@@ -131,6 +133,18 @@ function PaneToggleRow({
131
133
 
132
134
  export interface SeoEditorPaneProps {
133
135
  entityType: 'page' | 'post'
136
+ /**
137
+ * Sections for the page-level SEO/GEO audit. Omit to hide the twin rings —
138
+ * the audit reads structure (stats, quotes, image slots), which a post body
139
+ * alone cannot supply.
140
+ */
141
+ auditSections?: PageSection[]
142
+ /**
143
+ * Reports the audit upward so the rail's tab dot can show the same number
144
+ * this panel shows. Without it the dot and the panel compute independently
145
+ * and disagree — which is exactly what they did before the audit existed.
146
+ */
147
+ onAuditChange?: (audit: PageAuditResult | null) => void
134
148
  /** When absent, draft-only mode — meta fields sync via `onDraftChange`. */
135
149
  entityId: string | null
136
150
  onClose: () => void
@@ -145,6 +159,8 @@ export interface SeoEditorPaneProps {
145
159
  export function SeoEditorPane({
146
160
  entityType,
147
161
  entityId,
162
+ auditSections,
163
+ onAuditChange,
148
164
  onClose,
149
165
  onSaved,
150
166
  contentHtml = '',
@@ -165,6 +181,7 @@ export function SeoEditorPane({
165
181
  const [form, setForm] = useState<SeoFieldPayload>({})
166
182
  const [previewDevice, setPreviewDevice] = useState<'mobile' | 'desktop'>('mobile')
167
183
  const [socialPlatform, setSocialPlatform] = useState<'facebook' | 'twitter'>('facebook')
184
+ const [imagePickerOpen, setImagePickerOpen] = useState(false)
168
185
  const [remoteAnalysis, setRemoteAnalysis] = useState<SEOAnalysisResult | null>(null)
169
186
 
170
187
  useEffect(() => {
@@ -198,7 +215,24 @@ export function SeoEditorPane({
198
215
  .then((rec) => {
199
216
  if (!alive) return
200
217
  if (!rec) {
201
- setError('Could not load this record.')
218
+ // Same situation as the catch below, reached without a throw.
219
+ setRecord(null)
220
+ setForm({
221
+ metaTitle: draftMeta?.seoTitle ?? '',
222
+ metaDescription: draftMeta?.seoDescription ?? '',
223
+ focusKeyword: '',
224
+ keyTakeaway: '',
225
+ ogTitle: '',
226
+ ogDescription: '',
227
+ canonicalUrl: '',
228
+ structuredDataType: 'WebPage',
229
+ noindex: false,
230
+ nofollow: false,
231
+ llmsInclude: 'AUTO',
232
+ llmsDescription: '',
233
+ llmsSection: '',
234
+ llmsOptional: false,
235
+ })
202
236
  return
203
237
  }
204
238
  setRecord(rec)
@@ -210,6 +244,7 @@ export function SeoEditorPane({
210
244
  keyTakeaway: rec.keyTakeaway ?? '',
211
245
  ogTitle: rec.ogTitle ?? '',
212
246
  ogDescription: rec.ogDescription ?? '',
247
+ ogImage: rec.ogImage ?? '',
213
248
  twitterTitle: rec.twitterTitle ?? '',
214
249
  twitterDescription: rec.twitterDescription ?? '',
215
250
  structuredDataType: rec.structuredDataType ?? 'WebPage',
@@ -221,12 +256,36 @@ export function SeoEditorPane({
221
256
  llmsOptional: rec.llmsOptional ?? false,
222
257
  })
223
258
  })
224
- .catch((e: unknown) => alive && setError(e instanceof Error ? e.message : 'Failed to load'))
259
+ .catch(() => {
260
+ if (!alive) return
261
+ // No record is not a failure. Older cms-core resolves this endpoint
262
+ // through a PUBLISHED-only pipeline, so a saved draft 404s here even
263
+ // though the matching PUT accepts one — showing an error where the form
264
+ // belongs made a draft's SEO uneditable. Seed from the document's own
265
+ // meta instead; saving creates the record.
266
+ setRecord(null)
267
+ setForm({
268
+ metaTitle: draftMeta?.seoTitle ?? '',
269
+ metaDescription: draftMeta?.seoDescription ?? '',
270
+ focusKeyword: '',
271
+ keyTakeaway: '',
272
+ ogTitle: '',
273
+ ogDescription: '',
274
+ canonicalUrl: '',
275
+ structuredDataType: 'WebPage',
276
+ noindex: false,
277
+ nofollow: false,
278
+ llmsInclude: 'AUTO',
279
+ llmsDescription: '',
280
+ llmsSection: '',
281
+ llmsOptional: false,
282
+ })
283
+ })
225
284
  .finally(() => alive && setLoading(false))
226
285
  return () => {
227
286
  alive = false
228
287
  }
229
- }, [entityType, entityId, isDraft])
288
+ }, [entityType, entityId, isDraft, draftMeta?.seoTitle, draftMeta?.seoDescription])
230
289
 
231
290
  useEffect(() => {
232
291
  if (isDraft || contentHtml.trim() || !entityId) return
@@ -374,6 +433,52 @@ export function SeoEditorPane({
374
433
  const url = record?.url ?? (slug ? `/${slug.replace(/^\//, '')}` : '')
375
434
  const displayUrl = record?.url ?? url
376
435
 
436
+ /**
437
+ * Page-level audit, distinct from `analyzeContent` below: binary checks over
438
+ * page structure and metadata, spanning a true 0-100. `analyzeContent` stays
439
+ * as the prose analysis (readability, density) it is good at.
440
+ *
441
+ * Built from LIVE form state so the score moves as the editor types. See
442
+ * `buildPageAuditInput` for which fields feed which check, and for why the
443
+ * OG image and author are reported as unknown rather than missing.
444
+ */
445
+ const audit = useMemo(() => {
446
+ if (!auditSections) return null
447
+ return auditPage(
448
+ buildPageAuditInput({
449
+ sections: auditSections,
450
+ // LIVE form state, not the saved record — the score has to move as the
451
+ // editor types. The page editor builds the same input from the saved
452
+ // record for its first-paint score; routing both through one builder is
453
+ // what keeps the two numbers from drifting apart.
454
+ meta: {
455
+ metaTitle: form.metaTitle ?? '',
456
+ metaDescription: form.metaDescription ?? '',
457
+ focusKeyword: form.focusKeyword ?? '',
458
+ structuredDataType: form.structuredDataType ?? '',
459
+ noindex: Boolean(form.noindex),
460
+ // Directives are edited in the Page tab, so this pane has no live
461
+ // value to offer — it reads the saved one. Defaulting to `false`
462
+ // instead would tell the GEO gate that snippets are allowed on a page
463
+ // that has turned them off.
464
+ nosnippet: Boolean(record?.nosnippet),
465
+ llmsInclude: form.llmsInclude ?? 'AUTO',
466
+ // Live value, so the check flips the moment an image is chosen or
467
+ // cleared rather than waiting for a save round-trip. `??` and not
468
+ // `||`: an empty string is a deliberate removal, not "unset".
469
+ hasOgImage: Boolean(form.ogImage ?? record?.ogImage),
470
+ },
471
+ slug,
472
+ record,
473
+ fallbackTitle: draftMeta?.seoTitle,
474
+ }),
475
+ )
476
+ }, [auditSections, form, slug, record, draftMeta?.seoTitle])
477
+
478
+ useEffect(() => {
479
+ onAuditChange?.(audit)
480
+ }, [audit, onAuditChange])
481
+
377
482
  const analysisInput = useMemo(
378
483
  () => ({
379
484
  title: record?.title ?? draftMeta?.seoTitle ?? '',
@@ -389,7 +494,7 @@ export function SeoEditorPane({
389
494
  canonical: form.canonicalUrl,
390
495
  ogTitle: form.ogTitle,
391
496
  ogDescription: form.ogDescription,
392
- ogImage: record?.ogImage ?? undefined,
497
+ ogImage: (form.ogImage ?? record?.ogImage) || undefined,
393
498
  }),
394
499
  [record, draftMeta, slug, contentHtml, form, renderedTitle],
395
500
  )
@@ -426,6 +531,8 @@ export function SeoEditorPane({
426
531
  )
427
532
  }
428
533
 
534
+ const shareImage = form.ogImage ?? record?.ogImage ?? ''
535
+
429
536
  const socialTitle =
430
537
  socialPlatform === 'twitter'
431
538
  ? form.twitterTitle || form.ogTitle || renderedTitle
@@ -447,6 +554,7 @@ export function SeoEditorPane({
447
554
  </InspectorPaneHeader>
448
555
 
449
556
  <InspectorPaneBody>
557
+ {audit && !loading && <PageAuditPanel audit={audit} />}
450
558
  {loading ? (
451
559
  <div className="px-5 py-4">
452
560
  <SeoLoading />
@@ -520,7 +628,11 @@ export function SeoEditorPane({
520
628
  label="Key takeaway"
521
629
  hint={
522
630
  <span className="flex items-center gap-2">
523
- <CharCount value={form.keyTakeaway ?? ''} max={KEY_TAKEAWAY_MAX} />
631
+ <SeoCharCount
632
+ value={form.keyTakeaway ?? ''}
633
+ max={KEY_TAKEAWAY_MAX}
634
+ bar={false}
635
+ />
524
636
  {aiButton('keyTakeaway')}
525
637
  </span>
526
638
  }
@@ -564,18 +676,21 @@ export function SeoEditorPane({
564
676
  </button>
565
677
  </div>
566
678
  </div>
567
- <div
568
- className={`rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] p-3 ${previewDevice === 'mobile' ? 'max-w-[320px]' : ''}`}
569
- >
570
- <p className="truncate text-[11px] text-[var(--suc)]">{displayUrl}</p>
571
- <p className="text-[15px] leading-snug text-[var(--acc)]">{renderedTitle}</p>
572
- <p className="line-clamp-2 text-[12px] text-[var(--sub)]">{desc}</p>
679
+ <div className={previewDevice === 'mobile' ? 'max-w-[320px]' : ''}>
680
+ {/* The title previously rendered in the brand accent, which
681
+ made the card a preview of the admin rather than of a
682
+ search result. */}
683
+ <SerpPreview title={renderedTitle} url={displayUrl} description={desc} />
573
684
  </div>
574
685
  <PaneField
575
686
  label="SEO title"
576
687
  hint={
577
688
  <span className="flex items-center gap-2">
578
- <CharCount value={form.metaTitle ?? ''} max={META_TITLE_MAX} />
689
+ <SeoCharCount
690
+ value={form.metaTitle ?? ''}
691
+ min={META_TITLE_MIN}
692
+ max={META_TITLE_MAX}
693
+ />
579
694
  {aiButton('metaTitle')}
580
695
  </span>
581
696
  }
@@ -598,7 +713,11 @@ export function SeoEditorPane({
598
713
  label="Meta description"
599
714
  hint={
600
715
  <span className="flex items-center gap-2">
601
- <CharCount value={form.metaDescription ?? ''} max={META_DESC_MAX} />
716
+ <SeoCharCount
717
+ value={form.metaDescription ?? ''}
718
+ min={META_DESC_MIN}
719
+ max={META_DESC_MAX}
720
+ />
602
721
  {aiButton('metaDescription')}
603
722
  </span>
604
723
  }
@@ -613,7 +732,19 @@ export function SeoEditorPane({
613
732
  {alignmentRow('metaDescription')}
614
733
  </SeoAccordionItem>
615
734
 
616
- <SeoAccordionItem value="social-appearance" title="Social appearance">
735
+ <SeoAccordionItem
736
+ value="social-appearance"
737
+ title="Social appearance"
738
+ badge={
739
+ shareImage ? (
740
+ <SeoAccordionBadge tone="good">Image set</SeoAccordionBadge>
741
+ ) : (
742
+ // A shared link with no image is the single biggest drop in
743
+ // engagement, so it is worth stating while collapsed.
744
+ <SeoAccordionBadge tone="warn">No image</SeoAccordionBadge>
745
+ )
746
+ }
747
+ >
617
748
  <p className="text-[11.5px] text-[var(--sub)]">
618
749
  Customize title, description, and image used when sharing on social platforms.
619
750
  </p>
@@ -630,19 +761,34 @@ export function SeoEditorPane({
630
761
  </button>
631
762
  ))}
632
763
  </div>
764
+ {/*
765
+ The share preview and the upload target are one object: the
766
+ card an editor is judging is the control they change it with.
767
+ Previously the image was display-only — there was no way to
768
+ set an OG image from the SEO editor at all.
769
+ */}
633
770
  <div className="overflow-hidden rounded-[7px] border border-[var(--bdr)]">
634
- {record?.ogImage ? (
771
+ {shareImage ? (
635
772
  // eslint-disable-next-line @next/next/no-img-element
636
773
  <img
637
- src={record.ogImage}
774
+ src={shareImage}
638
775
  alt=""
639
- className="aspect-[1200/630] w-full bg-[var(--bg)] object-cover"
776
+ className="aspect-1200/630 w-full bg-[var(--bg)] object-cover"
640
777
  />
641
778
  ) : (
642
- <div className="flex aspect-[1200/630] w-full items-center justify-center bg-[var(--bg)] text-[12px] text-[var(--muted)]">
643
- No social image — add a featured or OG image
644
- </div>
779
+ <button
780
+ type="button"
781
+ onClick={() => setImagePickerOpen(true)}
782
+ className="flex aspect-1200/630 w-full flex-col items-center justify-center gap-1 bg-[var(--bg)] text-[12px] text-[var(--muted)] transition-colors hover:bg-[var(--acc-l)] hover:text-[var(--acc)]"
783
+ >
784
+ <ImagePlus className="h-5 w-5" aria-hidden />
785
+ Choose a share image
786
+ <span className="text-[10.5px]">1200×630 or wider, about 1.91:1</span>
787
+ </button>
645
788
  )}
789
+ {/* Domain / title / description sit between the frame and the
790
+ toolbar so the card reads as the shared post, not as a
791
+ form with a thumbnail on top. */}
646
792
  <div className="p-2.5">
647
793
  <p className="truncate text-[10.5px] text-[var(--muted)] uppercase">
648
794
  {displayUrl.replace(/^https?:\/\//, '').split('/')[0] || 'yoursite.com'}
@@ -652,6 +798,30 @@ export function SeoEditorPane({
652
798
  </p>
653
799
  <p className="line-clamp-2 text-[12px] text-[var(--sub)]">{socialDesc}</p>
654
800
  </div>
801
+ {/* Always visible, never hover-only: a control an editor has
802
+ to discover by hovering is a control most never find. */}
803
+ <div className="flex items-center gap-2 border-t border-[var(--bdr)] bg-[var(--bg)] px-2.5 py-2">
804
+ <span className="min-w-0 flex-1 truncate text-[11.5px] text-[var(--sub)]">
805
+ {shareImage ? shareImage.split('/').pop() : 'No image selected'}
806
+ </span>
807
+ <button
808
+ type="button"
809
+ onClick={() => setImagePickerOpen(true)}
810
+ className="shrink-0 rounded-[5px] px-2 py-1 text-[11.5px] font-medium text-[var(--acc)] transition-colors hover:bg-[var(--acc-l)]"
811
+ >
812
+ {shareImage ? 'Replace' : 'Choose'}
813
+ </button>
814
+ {shareImage && (
815
+ <button
816
+ type="button"
817
+ onClick={() => update({ ogImage: '' })}
818
+ aria-label="Remove share image"
819
+ className="shrink-0 rounded-[5px] p-1 text-[var(--muted)] transition-colors hover:bg-[var(--err-l)] hover:text-[var(--err)]"
820
+ >
821
+ <Trash2 className="h-3.5 w-3.5" aria-hidden />
822
+ </button>
823
+ )}
824
+ </div>
655
825
  </div>
656
826
  {socialPlatform === 'facebook' ? (
657
827
  <>
@@ -775,7 +945,13 @@ export function SeoEditorPane({
775
945
  </PaneField>
776
946
  <PaneField
777
947
  label="Agent description"
778
- hint={<CharCount value={form.llmsDescription ?? ''} max={LLMS_DESC_MAX} />}
948
+ hint={
949
+ <SeoCharCount
950
+ value={form.llmsDescription ?? ''}
951
+ max={LLMS_DESC_MAX}
952
+ bar={false}
953
+ />
954
+ }
779
955
  >
780
956
  <textarea
781
957
  className={`${inputCls} min-h-16 resize-y`}
@@ -808,6 +984,12 @@ export function SeoEditorPane({
808
984
  </SeoAccordion>
809
985
  </>
810
986
  )}
987
+ <MediaPickerModal
988
+ open={imagePickerOpen}
989
+ onClose={() => setImagePickerOpen(false)}
990
+ accept="image/*"
991
+ onSelect={(url) => update({ ogImage: url })}
992
+ />
811
993
  </InspectorPaneBody>
812
994
 
813
995
  <InspectorPaneFooter>
@@ -0,0 +1,41 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * A search-result preview for the SEO editor.
5
+ *
6
+ * Renders the three things a searcher actually sees, in the order and weight
7
+ * they see them, so an editor can judge a title and description as a result
8
+ * rather than as two form fields.
9
+ *
10
+ * The link blue is a literal, not a token: it is Google's result colour, and
11
+ * the point of the card is to look like the thing being previewed. Tokenising
12
+ * it would let a brand accent repaint it and quietly stop it being a preview.
13
+ * `#8ab4f8` is the dark-scheme equivalent.
14
+ */
15
+ export interface SerpPreviewProps {
16
+ title: string
17
+ /** Full resolved URL, shown above the title as search engines display it. */
18
+ url: string
19
+ description: string
20
+ }
21
+
22
+ export function SerpPreview({ title, url, description }: SerpPreviewProps) {
23
+ return (
24
+ <div
25
+ className="border-border bg-card rounded-lg border px-3 py-[11px]"
26
+ role="group"
27
+ aria-label="Search result preview"
28
+ >
29
+ <p className="truncate text-[11px] text-emerald-700 dark:text-emerald-400">
30
+ {url || 'https://example.com/page'}
31
+ </p>
32
+ <p className="truncate text-[14px] leading-snug text-[#1a0dab] dark:text-[#8ab4f8]">
33
+ {title || 'Untitled page'}
34
+ </p>
35
+ {/* Two lines is what a result gets; more written here simply is not read. */}
36
+ <p className="text-muted-foreground line-clamp-2 text-[11.5px] leading-snug">
37
+ {description || 'No meta description set. Search engines will improvise one.'}
38
+ </p>
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,98 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import type { PageSection } from '@prenta/core/page-sections'
5
+ import { auditPage, type PageAuditResult } from '@prenta/core/seo/page-audit'
6
+ import { fetchSeoContentRecord, type SeoContentRecord } from '../lib/seo-service.js'
7
+ import { buildPageAuditInput, metaFromRecord } from '../lib/page-audit-input.js'
8
+
9
+ export interface UsePageAuditArgs {
10
+ entityType: 'page' | 'post'
11
+ /** Null for an unsaved document — there is no record to audit against yet. */
12
+ entityId: string | null
13
+ sections: PageSection[]
14
+ slug: string
15
+ /**
16
+ * The document's own meta, used when no SEO record exists yet. A page that
17
+ * has never had SEO edited still has a title, a description and sections —
18
+ * it is scoreable, and refusing to score it leaves the header chip blank on
19
+ * exactly the pages that most need the nudge.
20
+ */
21
+ fallback?: { title?: string; description?: string }
22
+ }
23
+
24
+ /**
25
+ * The page's audit, from its saved SEO record — or from the document's own
26
+ * fields when no record exists yet, so every page has a number.
27
+ *
28
+ * This exists so the header chip and the rail's tab dot have a score from first
29
+ * paint. Previously the audit was computed inside the SEO pane, which mounts
30
+ * only when its tab is opened — so before that the chip showed a number from a
31
+ * different scorer with different bands, and the header read 53 beside rings
32
+ * reading 0 and 57.
33
+ *
34
+ * While the SEO pane is open it reports a LIVE audit built from unsaved form
35
+ * state, which supersedes this one. Both go through `buildPageAuditInput`, so
36
+ * they differ only in saved-versus-unsaved metadata — the difference an editor
37
+ * expects to see — and never in how a check is decided.
38
+ */
39
+ export function usePageAudit({
40
+ entityType,
41
+ entityId,
42
+ sections,
43
+ slug,
44
+ fallback,
45
+ }: UsePageAuditArgs): {
46
+ audit: PageAuditResult
47
+ record: SeoContentRecord | null
48
+ reload: () => void
49
+ } {
50
+ const [record, setRecord] = useState<SeoContentRecord | null>(null)
51
+ const [nonce, setNonce] = useState(0)
52
+
53
+ useEffect(() => {
54
+ if (!entityId) {
55
+ setRecord(null)
56
+ return
57
+ }
58
+ let alive = true
59
+ fetchSeoContentRecord(entityType, entityId)
60
+ .then((rec) => {
61
+ if (alive) setRecord(rec)
62
+ })
63
+ .catch(() => {
64
+ // A page that has never had SEO saved has no record. That is not an
65
+ // error worth surfacing here — the audit simply reports what it can,
66
+ // and the SEO pane shows its own error state if the user opens it.
67
+ if (alive) setRecord(null)
68
+ })
69
+ return () => {
70
+ alive = false
71
+ }
72
+ }, [entityType, entityId, nonce])
73
+
74
+ /**
75
+ * With a record, audit the saved SEO. Without one, audit the document's own
76
+ * fields and treat the record-only inputs as unset — which is honest: no
77
+ * focus keyword has been chosen, no schema has been bound, and nothing has
78
+ * set noindex. The score is real, and it improves the moment SEO is filled in.
79
+ */
80
+ const meta = record
81
+ ? metaFromRecord(record)
82
+ : {
83
+ metaTitle: fallback?.title ?? '',
84
+ metaDescription: fallback?.description ?? '',
85
+ focusKeyword: '',
86
+ structuredDataType: '',
87
+ noindex: false,
88
+ nosnippet: false,
89
+ llmsInclude: 'AUTO',
90
+ hasOgImage: false,
91
+ }
92
+
93
+ const audit = auditPage(buildPageAuditInput({ sections, meta, slug, record }))
94
+
95
+ const reload = useCallback(() => setNonce((n) => n + 1), [])
96
+
97
+ return { audit, record, reload }
98
+ }
@@ -0,0 +1,96 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import type { BreakpointId } from '../lib/breakpoints.js'
5
+ import {
6
+ probeFieldReflow,
7
+ reflowSeverity,
8
+ worstBreakpoint,
9
+ type FieldLineCount,
10
+ type ReflowSeverity,
11
+ } from '../lib/reflow-probe.js'
12
+
13
+ export interface ReflowRow extends FieldLineCount {
14
+ /** Lines this field ran to in the published version. */
15
+ was: number
16
+ /** `lines - was`. Only growth is treated as a problem. */
17
+ diff: number
18
+ state: ReflowSeverity
19
+ }
20
+
21
+ export interface ReflowGuardResult {
22
+ rows: ReflowRow[]
23
+ /** Breakpoints where the field now wraps to more lines than published. */
24
+ grown: ReflowRow[]
25
+ /** The one worth proofing on, or `null` when nothing grew. */
26
+ worst: BreakpointId | null
27
+ /** Character delta against the published string; 0 when there is no baseline. */
28
+ delta: number
29
+ }
30
+
31
+ const EMPTY: ReflowGuardResult = { rows: [], grown: [], worst: null, delta: 0 }
32
+
33
+ /**
34
+ * Measures how one field wraps at every breakpoint, and how that compares to
35
+ * what is published.
36
+ *
37
+ * ── Why it runs on a delay ────────────────────────────────────────────────
38
+ *
39
+ * A pass clones the canvas and lays it out twelve times (six widths, current
40
+ * and published). That is far too much to do per keystroke, and it is also
41
+ * pointless — mid-word text tells an editor nothing. So it settles first.
42
+ *
43
+ * `null` from the probe means the field is not measurable: the section may be
44
+ * rendered by a consumer's own component that carries no `data-cms-field`
45
+ * markers. The guard then reports nothing rather than a number read off the
46
+ * wrong element — an editor treats a line count as a fact, and a confident
47
+ * wrong one is worse than a blank.
48
+ */
49
+ export function useReflowGuard(options: {
50
+ canvas: HTMLElement | null
51
+ sectionId: string
52
+ fieldPath: string
53
+ /** Current field text. Only used to retrigger — the probe reads the DOM. */
54
+ value: string
55
+ /** Published text, or `undefined` when the field has never been published. */
56
+ baseline: string | undefined
57
+ /** Skip measuring entirely (field type isn't guarded, panel is collapsed). */
58
+ enabled?: boolean
59
+ delayMs?: number
60
+ }): ReflowGuardResult {
61
+ const { canvas, sectionId, fieldPath, value, baseline, enabled = true, delayMs = 200 } = options
62
+ const [result, setResult] = useState<ReflowGuardResult>(EMPTY)
63
+
64
+ useEffect(() => {
65
+ if (!enabled || !canvas) {
66
+ setResult(EMPTY)
67
+ return
68
+ }
69
+
70
+ const timer = setTimeout(() => {
71
+ const probed = probeFieldReflow({ canvas, sectionId, fieldPath, baselineText: baseline })
72
+ if (!probed) {
73
+ setResult(EMPTY)
74
+ return
75
+ }
76
+
77
+ const rows: ReflowRow[] = probed.current.map((row) => {
78
+ const was = probed.baseline.find((b) => b.id === row.id)?.lines ?? row.lines
79
+ return { ...row, was, diff: row.lines - was, state: reflowSeverity(row.lines, was) }
80
+ })
81
+
82
+ setResult({
83
+ rows,
84
+ grown: rows.filter((r) => r.diff > 0),
85
+ worst: worstBreakpoint(probed.current, probed.baseline),
86
+ delta: baseline === undefined ? 0 : value.length - baseline.length,
87
+ })
88
+ }, delayMs)
89
+
90
+ return () => clearTimeout(timer)
91
+ // `value` is a dependency because the canvas re-renders as it changes —
92
+ // the probe reads the resulting DOM rather than the string itself.
93
+ }, [canvas, sectionId, fieldPath, value, baseline, enabled, delayMs])
94
+
95
+ return result
96
+ }
@@ -71,6 +71,16 @@ export function AdminShell({
71
71
  : { display: 'block', position: 'relative' }
72
72
  }
73
73
  >
74
+ {/*
75
+ * First focusable thing in the admin, so a keyboard user's very first
76
+ * Tab offers a jump past the sidebar's ~20 nav items. Off-screen until
77
+ * focused; styling lives in `responsive.css` because it has to survive
78
+ * a consumer whose Tailwind build does not scan this file.
79
+ */}
80
+ <a className="prenta-skip-link" href="#prenta-main">
81
+ Skip to content
82
+ </a>
83
+
74
84
  {!isDesktop && mobileSidebarOpen && (
75
85
  <div
76
86
  aria-hidden
@@ -111,7 +121,9 @@ export function AdminShell({
111
121
  >
112
122
  {header}
113
123
  {breadcrumbs}
114
- <main className="min-h-0 flex-1 overflow-y-auto">{children}</main>
124
+ <main id="prenta-main" className="min-h-0 flex-1 overflow-y-auto">
125
+ {children}
126
+ </main>
115
127
  </div>
116
128
  </div>
117
129
  )