@prenta/admin 0.92.0 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/CHANGELOG.md +552 -0
  2. package/design-system/tokens.css +173 -71
  3. package/design-system/tokens.json +244 -63
  4. package/design-system/tokens.scss +203 -98
  5. package/design-system/tokens.tailwind.cjs +93 -38
  6. package/dist/AdminRoot.d.ts +27 -6
  7. package/dist/AdminRoot.d.ts.map +1 -1
  8. package/dist/AdminRoot.js.map +1 -1
  9. package/dist/__tests__/components/breakpoint-controls.render.test.d.ts +2 -0
  10. package/dist/__tests__/components/breakpoint-controls.render.test.d.ts.map +1 -0
  11. package/dist/__tests__/components/breakpoint-controls.render.test.js +51 -0
  12. package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -0
  13. package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts +2 -0
  14. package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts.map +1 -0
  15. package/dist/__tests__/components/canvas-edit-tip.render.test.js +37 -0
  16. package/dist/__tests__/components/canvas-edit-tip.render.test.js.map +1 -0
  17. package/dist/__tests__/components/editor-rail.render.test.d.ts +2 -0
  18. package/dist/__tests__/components/editor-rail.render.test.d.ts.map +1 -0
  19. package/dist/__tests__/components/editor-rail.render.test.js +138 -0
  20. package/dist/__tests__/components/editor-rail.render.test.js.map +1 -0
  21. package/dist/__tests__/components/media-block.render.test.d.ts +2 -0
  22. package/dist/__tests__/components/media-block.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/components/media-block.render.test.js +108 -0
  24. package/dist/__tests__/components/media-block.render.test.js.map +1 -0
  25. package/dist/__tests__/components/page-audit-panel.render.test.d.ts +2 -0
  26. package/dist/__tests__/components/page-audit-panel.render.test.d.ts.map +1 -0
  27. package/dist/__tests__/components/page-audit-panel.render.test.js +59 -0
  28. package/dist/__tests__/components/page-audit-panel.render.test.js.map +1 -0
  29. package/dist/__tests__/components/reflow-meter.render.test.d.ts +2 -0
  30. package/dist/__tests__/components/reflow-meter.render.test.d.ts.map +1 -0
  31. package/dist/__tests__/components/reflow-meter.render.test.js +94 -0
  32. package/dist/__tests__/components/reflow-meter.render.test.js.map +1 -0
  33. package/dist/__tests__/components/seo-char-count.test.js +24 -6
  34. package/dist/__tests__/components/seo-char-count.test.js.map +1 -1
  35. package/dist/__tests__/components/serp-preview.render.test.d.ts +2 -0
  36. package/dist/__tests__/components/serp-preview.render.test.d.ts.map +1 -0
  37. package/dist/__tests__/components/serp-preview.render.test.js +25 -0
  38. package/dist/__tests__/components/serp-preview.render.test.js.map +1 -0
  39. package/dist/__tests__/lib/breakpoints.test.d.ts +2 -0
  40. package/dist/__tests__/lib/breakpoints.test.d.ts.map +1 -0
  41. package/dist/__tests__/lib/breakpoints.test.js +67 -0
  42. package/dist/__tests__/lib/breakpoints.test.js.map +1 -0
  43. package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts +2 -0
  44. package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts.map +1 -0
  45. package/dist/__tests__/lib/canvas-edit-chrome.test.js +44 -0
  46. package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -0
  47. package/dist/__tests__/lib/media-format.test.d.ts +2 -0
  48. package/dist/__tests__/lib/media-format.test.d.ts.map +1 -0
  49. package/dist/__tests__/lib/media-format.test.js +70 -0
  50. package/dist/__tests__/lib/media-format.test.js.map +1 -0
  51. package/dist/__tests__/lib/page-audit-input.test.d.ts +2 -0
  52. package/dist/__tests__/lib/page-audit-input.test.d.ts.map +1 -0
  53. package/dist/__tests__/lib/page-audit-input.test.js +122 -0
  54. package/dist/__tests__/lib/page-audit-input.test.js.map +1 -0
  55. package/dist/__tests__/lib/page-editor-service.test.js +2 -0
  56. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
  57. package/dist/__tests__/lib/page-redirect.test.d.ts +2 -0
  58. package/dist/__tests__/lib/page-redirect.test.d.ts.map +1 -0
  59. package/dist/__tests__/lib/page-redirect.test.js +26 -0
  60. package/dist/__tests__/lib/page-redirect.test.js.map +1 -0
  61. package/dist/__tests__/lib/rail-width.test.d.ts +2 -0
  62. package/dist/__tests__/lib/rail-width.test.d.ts.map +1 -0
  63. package/dist/__tests__/lib/rail-width.test.js +69 -0
  64. package/dist/__tests__/lib/rail-width.test.js.map +1 -0
  65. package/dist/__tests__/lib/reflow-baseline.test.d.ts +2 -0
  66. package/dist/__tests__/lib/reflow-baseline.test.d.ts.map +1 -0
  67. package/dist/__tests__/lib/reflow-baseline.test.js +56 -0
  68. package/dist/__tests__/lib/reflow-baseline.test.js.map +1 -0
  69. package/dist/__tests__/lib/reflow-probe.test.d.ts +2 -0
  70. package/dist/__tests__/lib/reflow-probe.test.d.ts.map +1 -0
  71. package/dist/__tests__/lib/reflow-probe.test.js +73 -0
  72. package/dist/__tests__/lib/reflow-probe.test.js.map +1 -0
  73. package/dist/__tests__/styles/canvas-container-queries.test.d.ts +2 -0
  74. package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
  75. package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
  76. package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
  77. package/dist/__tests__/styles/packaging.test.d.ts +2 -0
  78. package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
  79. package/dist/__tests__/styles/packaging.test.js +31 -0
  80. package/dist/__tests__/styles/packaging.test.js.map +1 -0
  81. package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
  82. package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
  83. package/dist/__tests__/styles/tokens-drift.test.js +92 -0
  84. package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
  85. package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
  86. package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
  87. package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
  88. package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
  89. package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
  90. package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
  91. package/dist/__tests__/views/editor-top-bar.render.test.js +1 -1
  92. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  93. package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
  94. package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
  95. package/dist/__tests__/views/page-tab.render.test.js +151 -0
  96. package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
  97. package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
  98. package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
  99. package/dist/__tests__/views/section-class-parity.test.js +81 -0
  100. package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
  101. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
  102. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
  103. package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
  104. package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
  105. package/dist/components/BreakpointPicker.d.ts +18 -0
  106. package/dist/components/BreakpointPicker.d.ts.map +1 -0
  107. package/dist/components/BreakpointPicker.js +31 -0
  108. package/dist/components/BreakpointPicker.js.map +1 -0
  109. package/dist/components/CanvasEditTip.d.ts +6 -0
  110. package/dist/components/CanvasEditTip.d.ts.map +1 -0
  111. package/dist/components/CanvasEditTip.js +22 -0
  112. package/dist/components/CanvasEditTip.js.map +1 -0
  113. package/dist/components/EditorRail.d.ts +36 -0
  114. package/dist/components/EditorRail.d.ts.map +1 -0
  115. package/dist/components/EditorRail.js +104 -0
  116. package/dist/components/EditorRail.js.map +1 -0
  117. package/dist/components/EditorRailTabs.d.ts +32 -0
  118. package/dist/components/EditorRailTabs.d.ts.map +1 -0
  119. package/dist/components/EditorRailTabs.js +45 -0
  120. package/dist/components/EditorRailTabs.js.map +1 -0
  121. package/dist/components/MediaBlock.d.ts +49 -0
  122. package/dist/components/MediaBlock.d.ts.map +1 -0
  123. package/dist/components/MediaBlock.js +50 -0
  124. package/dist/components/MediaBlock.js.map +1 -0
  125. package/dist/components/ReflowMeter.d.ts +28 -0
  126. package/dist/components/ReflowMeter.d.ts.map +1 -0
  127. package/dist/components/ReflowMeter.js +42 -0
  128. package/dist/components/ReflowMeter.js.map +1 -0
  129. package/dist/components/ZoomChip.d.ts +17 -0
  130. package/dist/components/ZoomChip.d.ts.map +1 -0
  131. package/dist/components/ZoomChip.js +17 -0
  132. package/dist/components/ZoomChip.js.map +1 -0
  133. package/dist/components/breakpoint-icons.d.ts +3 -0
  134. package/dist/components/breakpoint-icons.d.ts.map +1 -0
  135. package/dist/components/breakpoint-icons.js +16 -0
  136. package/dist/components/breakpoint-icons.js.map +1 -0
  137. package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
  138. package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
  139. package/dist/components/page-tab/AuthorPicker.js +95 -0
  140. package/dist/components/page-tab/AuthorPicker.js.map +1 -0
  141. package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
  142. package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
  143. package/dist/components/page-tab/DirectiveSwitch.js +29 -0
  144. package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
  145. package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
  146. package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
  147. package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
  148. package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
  149. package/dist/components/seo/PageAuditPanel.d.ts +14 -0
  150. package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
  151. package/dist/components/seo/PageAuditPanel.js +59 -0
  152. package/dist/components/seo/PageAuditPanel.js.map +1 -0
  153. package/dist/components/seo/SeoCharCount.d.ts +19 -7
  154. package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
  155. package/dist/components/seo/SeoCharCount.js +30 -16
  156. package/dist/components/seo/SeoCharCount.js.map +1 -1
  157. package/dist/components/seo/SeoEditorPane.d.ts +15 -1
  158. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  159. package/dist/components/seo/SeoEditorPane.js +118 -21
  160. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  161. package/dist/components/seo/SerpPreview.d.ts +20 -0
  162. package/dist/components/seo/SerpPreview.d.ts.map +1 -0
  163. package/dist/components/seo/SerpPreview.js +6 -0
  164. package/dist/components/seo/SerpPreview.js.map +1 -0
  165. package/dist/hooks/usePageAudit.d.ts +41 -0
  166. package/dist/hooks/usePageAudit.d.ts.map +1 -0
  167. package/dist/hooks/usePageAudit.js +68 -0
  168. package/dist/hooks/usePageAudit.js.map +1 -0
  169. package/dist/hooks/useReflowGuard.d.ts +47 -0
  170. package/dist/hooks/useReflowGuard.d.ts.map +1 -0
  171. package/dist/hooks/useReflowGuard.js +52 -0
  172. package/dist/hooks/useReflowGuard.js.map +1 -0
  173. package/dist/lib/breakpoints.d.ts +51 -0
  174. package/dist/lib/breakpoints.d.ts.map +1 -0
  175. package/dist/lib/breakpoints.js +84 -0
  176. package/dist/lib/breakpoints.js.map +1 -0
  177. package/dist/lib/canvas-edit-chrome.d.ts +22 -0
  178. package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
  179. package/dist/lib/canvas-edit-chrome.js +81 -0
  180. package/dist/lib/canvas-edit-chrome.js.map +1 -0
  181. package/dist/lib/media-format.d.ts +55 -0
  182. package/dist/lib/media-format.d.ts.map +1 -0
  183. package/dist/lib/media-format.js +93 -0
  184. package/dist/lib/media-format.js.map +1 -0
  185. package/dist/lib/page-audit-input.d.ts +45 -0
  186. package/dist/lib/page-audit-input.d.ts.map +1 -0
  187. package/dist/lib/page-audit-input.js +53 -0
  188. package/dist/lib/page-audit-input.js.map +1 -0
  189. package/dist/lib/page-editor-service.d.ts +12 -0
  190. package/dist/lib/page-editor-service.d.ts.map +1 -1
  191. package/dist/lib/page-editor-service.js +6 -0
  192. package/dist/lib/page-editor-service.js.map +1 -1
  193. package/dist/lib/page-redirect.d.ts +39 -0
  194. package/dist/lib/page-redirect.d.ts.map +1 -0
  195. package/dist/lib/page-redirect.js +38 -0
  196. package/dist/lib/page-redirect.js.map +1 -0
  197. package/dist/lib/rail-width.d.ts +23 -0
  198. package/dist/lib/rail-width.d.ts.map +1 -0
  199. package/dist/lib/rail-width.js +59 -0
  200. package/dist/lib/rail-width.js.map +1 -0
  201. package/dist/lib/reflow-baseline.d.ts +45 -0
  202. package/dist/lib/reflow-baseline.d.ts.map +1 -0
  203. package/dist/lib/reflow-baseline.js +38 -0
  204. package/dist/lib/reflow-baseline.js.map +1 -0
  205. package/dist/lib/reflow-probe.d.ts +159 -0
  206. package/dist/lib/reflow-probe.d.ts.map +1 -0
  207. package/dist/lib/reflow-probe.js +228 -0
  208. package/dist/lib/reflow-probe.js.map +1 -0
  209. package/dist/lib/seo-service.d.ts +10 -0
  210. package/dist/lib/seo-service.d.ts.map +1 -1
  211. package/dist/lib/seo-service.js.map +1 -1
  212. package/dist/prenta-admin.css +1 -1
  213. package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
  214. package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
  215. package/dist/views/page-editor/CanvasMetaLine.js +23 -0
  216. package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
  217. package/dist/views/page-editor/EditorCanvas.d.ts +34 -8
  218. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  219. package/dist/views/page-editor/EditorCanvas.js +68 -34
  220. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  221. package/dist/views/page-editor/EditorTopBar.d.ts +14 -1
  222. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  223. package/dist/views/page-editor/EditorTopBar.js +8 -17
  224. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  225. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  226. package/dist/views/page-editor/PageSectionEditor.js +275 -53
  227. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  228. package/dist/views/page-editor/PageTab.d.ts +36 -0
  229. package/dist/views/page-editor/PageTab.d.ts.map +1 -0
  230. package/dist/views/page-editor/PageTab.js +65 -0
  231. package/dist/views/page-editor/PageTab.js.map +1 -0
  232. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  233. package/dist/views/page-editor/SectionInspector.js +63 -19
  234. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  235. package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
  236. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
  237. package/dist/views/page-editor/SectionsPanel.js +38 -7
  238. package/dist/views/page-editor/SectionsPanel.js.map +1 -1
  239. package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
  240. package/dist/views/page-editor/SitePreviewFrame.js +3 -0
  241. package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
  242. package/dist/views/page-editor/reflow-context.d.ts +28 -0
  243. package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
  244. package/dist/views/page-editor/reflow-context.js +14 -0
  245. package/dist/views/page-editor/reflow-context.js.map +1 -0
  246. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
  247. package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
  248. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
  249. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
  250. package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
  251. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
  252. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
  253. package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
  254. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
  255. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
  256. package/dist/views/page-editor/sections/GallerySection.js +7 -3
  257. package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
  258. package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
  259. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
  260. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
  261. package/dist/views/page-editor/sections/MissionSection.js +1 -1
  262. package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
  263. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
  264. package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
  265. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
  266. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
  267. package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
  268. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
  269. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  270. package/dist/views/page-editor/sections/SectionRenderer.js +49 -24
  271. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  272. package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
  273. package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
  274. package/dist/views/page-editor/sections/admin-renderers.js +27 -15
  275. package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
  276. package/dist/views/page-editor/sections/parts.d.ts +0 -1
  277. package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
  278. package/dist/views/page-editor/sections/parts.js +41 -16
  279. package/dist/views/page-editor/sections/parts.js.map +1 -1
  280. package/dist/views/page-editor/viewports.d.ts +7 -14
  281. package/dist/views/page-editor/viewports.d.ts.map +1 -1
  282. package/dist/views/page-editor/viewports.js +7 -12
  283. package/dist/views/page-editor/viewports.js.map +1 -1
  284. package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
  285. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  286. package/dist/views/post-editor/PostEditorCanvas.js +5 -2
  287. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  288. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  289. package/dist/views/post-editor/PostSectionEditor.js +2 -20
  290. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  291. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
  292. package/dist/views/post-editor/PostTemplateEditor.js +4 -1
  293. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
  294. package/package.json +5 -3
  295. package/src/AdminRoot.tsx +27 -6
  296. package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
  297. package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
  298. package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
  299. package/src/__tests__/components/media-block.render.test.tsx +126 -0
  300. package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
  301. package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
  302. package/src/__tests__/components/seo-char-count.test.tsx +26 -6
  303. package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
  304. package/src/__tests__/lib/breakpoints.test.ts +84 -0
  305. package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
  306. package/src/__tests__/lib/media-format.test.ts +89 -0
  307. package/src/__tests__/lib/page-audit-input.test.ts +144 -0
  308. package/src/__tests__/lib/page-editor-service.test.ts +2 -0
  309. package/src/__tests__/lib/page-redirect.test.ts +29 -0
  310. package/src/__tests__/lib/rail-width.test.ts +86 -0
  311. package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
  312. package/src/__tests__/lib/reflow-probe.test.ts +95 -0
  313. package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
  314. package/src/__tests__/styles/packaging.test.ts +38 -0
  315. package/src/__tests__/styles/tokens-drift.test.ts +110 -0
  316. package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
  317. package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
  318. package/src/__tests__/views/editor-top-bar.render.test.tsx +1 -1
  319. package/src/__tests__/views/page-tab.render.test.tsx +226 -0
  320. package/src/__tests__/views/section-class-parity.test.ts +98 -0
  321. package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
  322. package/src/components/BreakpointPicker.tsx +114 -0
  323. package/src/components/CanvasEditTip.tsx +46 -0
  324. package/src/components/EditorRail.tsx +219 -0
  325. package/src/components/EditorRailTabs.tsx +117 -0
  326. package/src/components/MediaBlock.tsx +223 -0
  327. package/src/components/ReflowMeter.tsx +125 -0
  328. package/src/components/ZoomChip.tsx +39 -0
  329. package/src/components/breakpoint-icons.ts +17 -0
  330. package/src/components/page-tab/AuthorPicker.tsx +240 -0
  331. package/src/components/page-tab/DirectiveSwitch.tsx +86 -0
  332. package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
  333. package/src/components/seo/PageAuditPanel.tsx +192 -0
  334. package/src/components/seo/SeoCharCount.tsx +55 -24
  335. package/src/components/seo/SeoEditorPane.tsx +213 -31
  336. package/src/components/seo/SerpPreview.tsx +41 -0
  337. package/src/hooks/usePageAudit.ts +98 -0
  338. package/src/hooks/useReflowGuard.ts +96 -0
  339. package/src/lib/breakpoints.ts +107 -0
  340. package/src/lib/canvas-edit-chrome.ts +82 -0
  341. package/src/lib/media-format.ts +99 -0
  342. package/src/lib/page-audit-input.ts +104 -0
  343. package/src/lib/page-editor-service.ts +18 -0
  344. package/src/lib/page-redirect.ts +60 -0
  345. package/src/lib/rail-width.ts +58 -0
  346. package/src/lib/reflow-baseline.ts +70 -0
  347. package/src/lib/reflow-probe.ts +299 -0
  348. package/src/lib/seo-service.ts +10 -0
  349. package/src/styles/prenta-tokens.css +120 -42
  350. package/src/styles/theme.css +130 -0
  351. package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
  352. package/src/views/page-editor/EditorCanvas.tsx +109 -50
  353. package/src/views/page-editor/EditorTopBar.tsx +35 -43
  354. package/src/views/page-editor/PageSectionEditor.tsx +419 -148
  355. package/src/views/page-editor/PageTab.tsx +433 -0
  356. package/src/views/page-editor/SectionInspector.tsx +82 -41
  357. package/src/views/page-editor/SectionsPanel.tsx +281 -140
  358. package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
  359. package/src/views/page-editor/reflow-context.ts +43 -0
  360. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
  361. package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
  362. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
  363. package/src/views/page-editor/sections/GallerySection.tsx +7 -3
  364. package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
  365. package/src/views/page-editor/sections/MissionSection.tsx +4 -2
  366. package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
  367. package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
  368. package/src/views/page-editor/sections/SectionRenderer.tsx +69 -29
  369. package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
  370. package/src/views/page-editor/sections/parts.tsx +46 -18
  371. package/src/views/page-editor/viewports.ts +13 -26
  372. package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
  373. package/src/views/post-editor/PostSectionEditor.tsx +1 -56
  374. package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
@@ -162,6 +162,29 @@
162
162
  --text-xs: var(--fs-hint);
163
163
  }
164
164
 
165
+ /*
166
+ * Canvas reflow steps — the container-query breakpoints the previewed PAGE
167
+ * reflows at, as distinct from Tailwind's `sm:`/`md:` viewport breakpoints,
168
+ * which describe the admin chrome around it.
169
+ *
170
+ * The canvas renders at a device's real CSS width and is scaled with a
171
+ * transform, so a viewport query there is meaningless: it would report the
172
+ * editor window's width, and picking "Mobile" on a wide monitor would render
173
+ * a 390px-wide canvas still laid out for desktop. Section renderers under
174
+ * `views/page-editor/sections/` must use `@pv-*`, never `sm:`/`md:`/`lg:`.
175
+ *
176
+ * Values come from the page-builder design handoff. Any element carrying
177
+ * these variants needs an ancestor with `@container` — EditorCanvas,
178
+ * PostEditorCanvas, PagePreview and PostPreview all declare it.
179
+ */
180
+ @theme {
181
+ --container-pv-sm: 460px; /* below: stats 1-up, buttons full width */
182
+ --container-pv-md: 700px; /* below: image+text stacks, quotes 1-up */
183
+ --container-pv-lg: 900px; /* below: quote grid 2-up, stats collapse */
184
+ --container-pv-xl: 1100px; /* below: general step-down */
185
+ --container-pv-2xl: 2000px; /* above: type and padding scale up */
186
+ }
187
+
165
188
  @layer base {
166
189
  .prenta-admin * {
167
190
  @apply border-border outline-ring/50;
@@ -255,6 +278,23 @@
255
278
  * The canvas re-establishes the site's typographic context: headings and
256
279
  * body text fall back to inheritance rather than the admin scale above.
257
280
  */
281
+ /*
282
+ * The canvas is the query container for every previewed section (`@pv-*`
283
+ * variants — see the step scale above). It lives here rather than as an
284
+ * `@container` utility on each host because FOUR surfaces render these
285
+ * sections — EditorCanvas, PostEditorCanvas, PagePreview, PostPreview — and
286
+ * a host that forgets it degrades silently: with no container ancestor,
287
+ * every `@pv-*` variant simply never matches and the page renders at its
288
+ * narrowest layout regardless of the selected breakpoint. Tying containment
289
+ * to the class that defines the surface makes that unforgettable.
290
+ *
291
+ * `inline-size` only: the canvas must stay free to grow as tall as its
292
+ * content, which `size` containment would prevent.
293
+ */
294
+ .prenta-admin .prenta-canvas {
295
+ container-type: inline-size;
296
+ }
297
+
258
298
  .prenta-admin .prenta-canvas :is(h1, h2, h3, h4) {
259
299
  font-size: revert-layer;
260
300
  font-weight: revert-layer;
@@ -383,6 +423,96 @@
383
423
  border-radius: var(--radius-md);
384
424
  margin: 1em 0;
385
425
  }
426
+
427
+ /*
428
+ * Structure canvas — lite hover outline on editable sections.
429
+ * Selected section chrome stays on the element (Tailwind outline-violet).
430
+ * Field-level chrome applies when renderers expose data-cms-field.
431
+ */
432
+ .prenta-admin .prenta-canvas [data-section-id][data-editable='true']:not([data-selected='true']) {
433
+ outline: 1px solid transparent;
434
+ outline-offset: -1px;
435
+ transition: outline-color 120ms ease;
436
+ }
437
+
438
+ .prenta-admin
439
+ .prenta-canvas
440
+ [data-section-id][data-editable='true']:not([data-selected='true']):hover {
441
+ outline-color: color-mix(in oklab, var(--acc) 40%, transparent);
442
+ }
443
+
444
+ .prenta-admin .prenta-canvas [data-cms-field] {
445
+ outline: 1px solid transparent;
446
+ outline-offset: 2px;
447
+ border-radius: 2px;
448
+ transition: outline-color 120ms ease;
449
+ cursor: text;
450
+ }
451
+
452
+ .prenta-admin .prenta-canvas [data-cms-field]:hover,
453
+ .prenta-admin .prenta-canvas [data-cms-field]:focus,
454
+ .prenta-admin .prenta-canvas [data-cms-field]:focus-within {
455
+ outline-color: color-mix(in oklab, var(--acc) 45%, transparent);
456
+ }
457
+
458
+ .prenta-admin .prenta-canvas [data-cms-field][data-cms-field-kind='media'] {
459
+ cursor: pointer;
460
+ }
461
+
462
+ @media (prefers-reduced-motion: reduce) {
463
+ .prenta-admin .prenta-canvas [data-section-id][data-editable='true'],
464
+ .prenta-admin .prenta-canvas [data-cms-field] {
465
+ transition: none;
466
+ }
467
+ }
468
+ }
469
+
470
+ /*
471
+ * Restore the stock Tailwind type scale inside the canvas.
472
+ *
473
+ * The `@theme inline` block above deliberately remaps six size keys onto admin
474
+ * design tokens so `text-base` means "admin body text, 13px" across the shell.
475
+ * That aliasing is right for admin chrome and wrong for the canvas, which
476
+ * renders the consumer's WEBSITE: there, `text-base` must mean 16px, the size
477
+ * a visitor actually reads. Left unscoped, every section renderer styles
478
+ * website copy on the admin scale — body text 3px small, `text-2xl` headings
479
+ * at 21px instead of 24px.
480
+ *
481
+ * This matters beyond looks. The reflow guard measures how copy wraps by
482
+ * laying it out in the canvas's own class chain; if the canvas renders type at
483
+ * the admin scale, every line count it reports is measured against a font size
484
+ * the live page never uses, and the guard is confidently wrong.
485
+ *
486
+ * Must live in `@layer utilities`, not `@layer base`: layer order beats
487
+ * specificity, so a base-layer rule — however specific — loses to the utility
488
+ * it is trying to correct. Inside the same layer, specificity decides, and
489
+ * three classes beat one.
490
+ *
491
+ * Only `font-size` is restated. The line-height half of each utility is a
492
+ * unitless ratio that scales correctly on its own.
493
+ *
494
+ * Values are Tailwind's defaults; the remapped keys are the only ones listed,
495
+ * because every other step (`text-3xl` and up) was never aliased.
496
+ */
497
+ @layer utilities {
498
+ .prenta-admin .prenta-canvas .text-xs {
499
+ font-size: 0.75rem;
500
+ }
501
+ .prenta-admin .prenta-canvas .text-sm {
502
+ font-size: 0.875rem;
503
+ }
504
+ .prenta-admin .prenta-canvas .text-base {
505
+ font-size: 1rem;
506
+ }
507
+ .prenta-admin .prenta-canvas .text-lg {
508
+ font-size: 1.125rem;
509
+ }
510
+ .prenta-admin .prenta-canvas .text-xl {
511
+ font-size: 1.25rem;
512
+ }
513
+ .prenta-admin .prenta-canvas .text-2xl {
514
+ font-size: 1.5rem;
515
+ }
386
516
  }
387
517
 
388
518
  .prenta-admin.reduce-motion,
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+
3
+ import { Laptop, Monitor, Smartphone, Tablet, type LucideIcon } from 'lucide-react'
4
+ import type { Breakpoint } from '../../lib/breakpoints.js'
5
+
6
+ const ICONS: Record<string, LucideIcon> = {
7
+ monitor: Monitor,
8
+ laptop: Laptop,
9
+ tablet: Tablet,
10
+ smartphone: Smartphone,
11
+ }
12
+
13
+ interface CanvasMetaLineProps {
14
+ breakpoint: Breakpoint
15
+ /** Unscaled page height in px, measured from the canvas surface. */
16
+ pageHeight: number
17
+ /** Current transform scale, 0–1. */
18
+ scale: number
19
+ }
20
+
21
+ /**
22
+ * Reads out what the canvas is currently showing:
23
+ * `▣ Wide Screen | 1920 × 2174 px | 42% scale`.
24
+ *
25
+ * The dimensions are the page's own, before scaling — the number an editor
26
+ * needs when reasoning about the live page. The scale is the separate fact
27
+ * that the preview is shrunk to fit.
28
+ */
29
+ export function CanvasMetaLine({ breakpoint, pageHeight, scale }: CanvasMetaLineProps) {
30
+ const Icon = ICONS[breakpoint.icon] ?? Monitor
31
+ const percent = Math.round(scale * 100)
32
+
33
+ return (
34
+ <p
35
+ className="text-muted-foreground mb-3 inline-flex items-center gap-1.5 text-[11px] font-semibold tracking-[0.3px] whitespace-nowrap"
36
+ // The canvas is decorative chrome around the real content; announcing
37
+ // these numbers on every keystroke would flood a screen reader.
38
+ aria-hidden
39
+ >
40
+ <Icon className="h-3 w-3" />
41
+ <span className="text-foreground/70">{breakpoint.name}</span>
42
+ <span aria-hidden>|</span>
43
+ <span className="tabular-nums">
44
+ {breakpoint.width} × {pageHeight ? Math.round(pageHeight) : '—'} px
45
+ </span>
46
+ <span aria-hidden>|</span>
47
+ <span className="tabular-nums">{percent}% scale</span>
48
+ </p>
49
+ )
50
+ }
@@ -1,71 +1,100 @@
1
1
  'use client'
2
2
 
3
3
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
4
- import { LayoutTemplate, Plus } from 'lucide-react'
4
+ import { LayoutTemplate } from 'lucide-react'
5
+ import { CanvasEditTip } from '../../components/CanvasEditTip.js'
6
+ import { CanvasMetaLine } from './CanvasMetaLine.js'
7
+ import { fitScale, getBreakpoint, type BreakpointId, type Zoom } from '../../lib/breakpoints.js'
5
8
  import type { PageSection } from '../../lib/page-editor-service.js'
6
9
  import { SectionRenderer } from './sections/index.js'
7
10
  import { StructureNotice } from './StructureNotice.js'
8
- import { getViewport, type ViewportId } from './viewports.js'
9
11
 
10
12
  interface EditorCanvasProps {
11
13
  sections: PageSection[]
12
14
  selectedId: string | null
13
- viewport: ViewportId
15
+ breakpoint: BreakpointId
16
+ zoom?: Zoom
14
17
  onSelect: (id: string) => void
15
- onAddSection: () => void
18
+ /**
19
+ * Reports the measured scale so the header's zoom chip can show a real
20
+ * percentage. Only the canvas knows the available width, so the number has
21
+ * to travel up rather than being recomputed by the header.
22
+ */
23
+ onScaleChange?: (scale: number) => void
24
+ /**
25
+ * Reports the canvas surface element, so the reflow guard can clone it. The
26
+ * guard needs the real element rather than a `document.querySelector`, which
27
+ * would also match the site-preview frame's canvas when that is mounted.
28
+ */
29
+ onCanvasElement?: (el: HTMLElement | null) => void
16
30
  /** Section types rendered as placeholders — shows the "Structure view" notice. */
17
31
  structuralTypes?: string[]
18
32
  sitePreviewAvailable?: boolean
19
33
  }
20
34
 
21
35
  /**
22
- * The preview canvas. Renders the page at the selected viewport's logical
23
- * width and scales it down with a CSS transform so wide viewports (1440px,
24
- * 1920px) fit inside the available canvas column. Scale is recomputed from
25
- * a ResizeObserver so it stays correct as panels open/close.
36
+ * The preview canvas.
37
+ *
38
+ * The canvas renders at the breakpoint's **real CSS width** and is scaled down
39
+ * with a transform. Combined with `container-type: inline-size`, that makes the
40
+ * page reflow against its own width — the same container queries that style the
41
+ * live page decide the layout. A max-width clamp cannot do this: it yields a
42
+ * narrow desktop layout rather than the mobile one.
43
+ *
44
+ * Two measurement rules, both learned the hard way:
45
+ *
46
+ * 1. Available width comes from a **zero-height sentinel** rendered as the
47
+ * panel's first child, not from `panel.clientWidth - padding`. The two
48
+ * disagree once a scrollbar appears or the host scales the preview, and
49
+ * the padding arithmetic silently rots whenever the gutter changes.
50
+ * 2. Both measurements are re-taken on **every render**, not only from the
51
+ * ResizeObserver. Page height changes with every text edit, image swap and
52
+ * show/hide — none of which resize the observed elements. The observer
53
+ * exists for later window resizes; its first callback is not a substitute.
26
54
  */
27
55
  export function EditorCanvas({
28
56
  sections,
29
57
  selectedId,
30
- viewport,
58
+ breakpoint,
59
+ zoom = 'fit',
31
60
  onSelect,
32
- onAddSection,
61
+ onScaleChange,
62
+ onCanvasElement,
33
63
  structuralTypes = [],
34
64
  sitePreviewAvailable = false,
35
65
  }: EditorCanvasProps) {
36
- const vp = getViewport(viewport)
37
- const containerRef = useRef<HTMLDivElement>(null)
66
+ const bp = getBreakpoint(breakpoint)
67
+ const sentinelRef = useRef<HTMLDivElement>(null)
38
68
  const surfaceRef = useRef<HTMLDivElement>(null)
39
69
  const sectionRefs = useRef<Map<string, HTMLElement>>(new Map())
40
- const [scale, setScale] = useState(1)
41
- const [surfaceHeight, setSurfaceHeight] = useState<number | null>(null)
70
+
71
+ const [available, setAvailable] = useState(0)
72
+ const [pageHeight, setPageHeight] = useState(0)
42
73
 
43
74
  const registerRef = useCallback((id: string, el: HTMLElement | null) => {
44
75
  if (el) sectionRefs.current.set(id, el)
45
76
  else sectionRefs.current.delete(id)
46
77
  }, [])
47
78
 
48
- // Recompute scale + scaled height from the container width.
79
+ // No dependency array: page height changes on edits that resize nothing the
80
+ // observer watches. Both setters bail when the value is unchanged, so this
81
+ // settles after one extra pass instead of looping.
49
82
  useLayoutEffect(() => {
50
- const container = containerRef.current
51
- if (!container) return
52
-
53
- const recompute = () => {
54
- // 48px breathing room so the surface never kisses the column edges.
55
- const available = container.clientWidth - 48
56
- const next = Math.min(1, available / vp.width)
57
- setScale(next > 0 ? next : 1)
58
- if (surfaceRef.current) {
59
- setSurfaceHeight(surfaceRef.current.offsetHeight * (next > 0 ? next : 1))
60
- }
83
+ const measure = () => {
84
+ const sentinelWidth = sentinelRef.current?.offsetWidth ?? 0
85
+ if (sentinelWidth > 0) setAvailable((prev) => (prev === sentinelWidth ? prev : sentinelWidth))
86
+
87
+ const height = surfaceRef.current?.offsetHeight ?? 0
88
+ if (height > 0) setPageHeight((prev) => (prev === height ? prev : height))
61
89
  }
62
90
 
63
- recompute()
64
- const ro = new ResizeObserver(recompute)
65
- ro.observe(container)
91
+ measure()
92
+
93
+ const ro = new ResizeObserver(measure)
94
+ if (sentinelRef.current) ro.observe(sentinelRef.current)
66
95
  if (surfaceRef.current) ro.observe(surfaceRef.current)
67
96
  return () => ro.disconnect()
68
- }, [vp.width, sections])
97
+ })
69
98
 
70
99
  // Scroll the selected section into view within the canvas.
71
100
  useEffect(() => {
@@ -74,28 +103,61 @@ export function EditorCanvas({
74
103
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
75
104
  }, [selectedId])
76
105
 
106
+ const scale = fitScale(available, bp.width, zoom)
107
+
108
+ useEffect(() => {
109
+ onScaleChange?.(scale)
110
+ }, [scale, onScaleChange])
111
+
77
112
  return (
78
- <div ref={containerRef} className="bg-muted h-full overflow-auto p-6">
113
+ <div className="bg-muted h-full overflow-auto px-6 pt-4.5 pb-10 text-center">
114
+ {/*
115
+ * Zero-height sentinel. Its offsetWidth IS the usable width — it is
116
+ * subject to exactly the same padding, scrollbar and host scaling as the
117
+ * canvas, so there is no arithmetic to get wrong. Must stay the first
118
+ * child and must stay zero-height.
119
+ */}
120
+ <div ref={sentinelRef} aria-hidden className="h-0" />
121
+
79
122
  {sections.length === 0 ? (
80
- <EmptyCanvas onAddSection={onAddSection} />
123
+ <EmptyCanvas />
81
124
  ) : (
82
125
  <>
126
+ <CanvasEditTip />
83
127
  <StructureNotice
84
128
  structuralTypes={structuralTypes}
85
129
  sitePreviewAvailable={sitePreviewAvailable}
86
130
  />
131
+ <CanvasMetaLine breakpoint={bp} pageHeight={pageHeight} scale={scale} />
132
+
133
+ {/*
134
+ * Fit box: reserves the scaled footprint so the surrounding scroll
135
+ * area sizes correctly against a transformed child. Deliberately has
136
+ * NO width/height transition — a transition frozen mid-flight (a
137
+ * preview that is not visible never finishes one) reports a bogus
138
+ * height and corrupts the next measurement.
139
+ */}
87
140
  <div
88
- className="mx-auto"
89
- style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
141
+ className="mx-auto block"
142
+ style={{
143
+ width: Math.round(bp.width * scale),
144
+ height: pageHeight ? Math.round(pageHeight * scale) : undefined,
145
+ }}
90
146
  >
91
147
  {/* `prenta-canvas` is the public hook for consumer brand CSS:
92
148
  site styles that should render in this preview scope to
93
149
  `.prenta-admin .prenta-canvas` instead of the admin root. */}
94
150
  <div
95
- ref={surfaceRef}
96
- className="prenta-canvas border-border bg-card origin-top overflow-hidden rounded-xl border shadow-sm"
151
+ ref={(el) => {
152
+ surfaceRef.current = el
153
+ onCanvasElement?.(el)
154
+ }}
155
+ // `prenta-canvas` carries `container-type: inline-size` (theme.css)
156
+ // — that is what makes the `@pv-*` steps in the section renderers
157
+ // resolve against the simulated width instead of the viewport.
158
+ className="prenta-canvas border-border bg-card origin-top-left overflow-hidden rounded-lg text-left shadow-[0_2px_20px_rgba(0,0,0,0.12)] motion-safe:transition-[width,transform] motion-safe:duration-(--mo-slow) motion-safe:ease-(--ease)"
97
159
  style={{
98
- width: vp.width,
160
+ width: bp.width,
99
161
  transform: `scale(${scale})`,
100
162
  transformOrigin: 'top left',
101
163
  }}
@@ -118,25 +180,22 @@ export function EditorCanvas({
118
180
  )
119
181
  }
120
182
 
121
- function EmptyCanvas({ onAddSection }: { onAddSection: () => void }) {
183
+ /**
184
+ * Design-owned pages always ship the template's sections, so an empty canvas
185
+ * means the template resolved to nothing — a configuration problem, not an
186
+ * invitation to add a section. There is deliberately no "Add section" button.
187
+ */
188
+ function EmptyCanvas() {
122
189
  return (
123
190
  <div className="mx-auto flex h-full max-w-md flex-col items-center justify-center text-center">
124
191
  <div className="bg-card border-border mb-4 flex h-14 w-14 items-center justify-center rounded-full border">
125
192
  <LayoutTemplate className="text-muted-foreground h-6 w-6" aria-hidden />
126
193
  </div>
127
- <h2 className="text-foreground text-lg font-medium">This page has no sections yet</h2>
128
- <p className="text-muted-foreground mt-1 mb-5 text-sm">
129
- Add your first section to start building the page. You can reorder, hide, and edit sections
130
- at any time.
194
+ <h2 className="text-foreground text-lg font-medium">This template renders no sections</h2>
195
+ <p className="text-muted-foreground mt-1 text-sm">
196
+ Layout is design-owned sections come from the page's template in code. Check that the page
197
+ has a template assigned, or pick a different one in Page settings.
131
198
  </p>
132
- <button
133
- type="button"
134
- onClick={onAddSection}
135
- className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
136
- >
137
- <Plus className="h-4 w-4" aria-hidden />
138
- Add Section
139
- </button>
140
199
  </div>
141
200
  )
142
201
  }
@@ -7,18 +7,16 @@ import {
7
7
  Eye,
8
8
  Globe,
9
9
  History,
10
- Laptop,
11
10
  Loader2,
12
- type LucideIcon,
13
- Monitor,
14
11
  Settings2,
15
- Smartphone,
16
- Tablet,
17
12
  TriangleAlert,
18
13
  } from 'lucide-react'
19
14
  import { EditorSeoScoreBadge } from '../../components/seo/EditorSeoScoreBadge.js'
15
+ import { BreakpointPicker } from '../../components/BreakpointPicker.js'
16
+ import { ZoomChip } from '../../components/ZoomChip.js'
20
17
  import type { PageStatus } from '../../lib/page-editor-service.js'
21
- import { VIEWPORTS, type ViewportId } from './viewports.js'
18
+ import type { Zoom } from '../../lib/breakpoints.js'
19
+ import { type ViewportId } from './viewports.js'
22
20
 
23
21
  export type SaveState = 'idle' | 'saving' | 'saved' | 'error'
24
22
 
@@ -35,6 +33,11 @@ interface EditorTopBarProps {
35
33
  /** Shown on hover/focus when publish is blocked by validation (not role). */
36
34
  publishDisabledReason?: string
37
35
  viewport: ViewportId
36
+ /** Canvas zoom mode; omit to hide the zoom chip (e.g. the site-preview iframe). */
37
+ zoom?: Zoom
38
+ /** Scale the canvas actually measured, 0–1 — displayed by the zoom chip. */
39
+ canvasScale?: number
40
+ onZoom?: (zoom: Zoom) => void
38
41
  /** Breadcrumb root label + back-button text (e.g. "Pages" or "Posts"). */
39
42
  backLabel?: string
40
43
  onViewport: (v: ViewportId) => void
@@ -63,7 +66,14 @@ interface EditorTopBarProps {
63
66
  onOpenSeo?: () => void
64
67
  seoOpen?: boolean
65
68
  /** Live 0–100 SEO score shown left of the SEO label. */
69
+ /**
70
+ * 0-100 for the header chip. `undefined` means not known yet — the chip
71
+ * renders without a number rather than defaulting to 0, which would read as
72
+ * a catastrophic score for a page nobody has audited.
73
+ */
66
74
  seoScore?: number
75
+ /** Band for `seoScore`; omit to derive from the content-analysis scale. */
76
+ seoScoreBand?: 'good' | 'fair' | 'poor'
67
77
  onSaveDraft: () => void
68
78
  onPublish: () => void
69
79
  /** Open the schedule-publish dialog. Omitted when the document is unsaved. */
@@ -81,13 +91,6 @@ interface EditorTopBarProps {
81
91
  structuralTypes?: string[]
82
92
  }
83
93
 
84
- const VIEWPORT_ICONS: Record<string, LucideIcon> = {
85
- monitor: Monitor,
86
- laptop: Laptop,
87
- tablet: Tablet,
88
- smartphone: Smartphone,
89
- }
90
-
91
94
  const STATUS_TEXT: Record<PageStatus, { label: string; className: string }> = {
92
95
  DRAFT: { label: 'Draft', className: 'text-warning dark:text-amber-400' },
93
96
  PUBLISHED: { label: 'Published', className: 'text-emerald-600 dark:text-emerald-400' },
@@ -106,6 +109,9 @@ export function EditorTopBar({
106
109
  publishReady = true,
107
110
  publishDisabledReason,
108
111
  viewport,
112
+ zoom,
113
+ canvasScale,
114
+ onZoom,
109
115
  backLabel = 'Pages',
110
116
  onViewport,
111
117
  onBack,
@@ -117,7 +123,8 @@ export function EditorTopBar({
117
123
  onOpenSettings,
118
124
  onOpenSeo,
119
125
  seoOpen = false,
120
- seoScore = 0,
126
+ seoScore,
127
+ seoScoreBand,
121
128
  onSaveDraft,
122
129
  onPublish,
123
130
  onSchedule,
@@ -209,32 +216,11 @@ export function EditorTopBar({
209
216
 
210
217
  {/* Right controls */}
211
218
  <div className="flex items-center gap-2">
212
- <div
213
- className="border-border bg-background hidden items-center gap-0.5 rounded-lg border p-0.5 md:flex"
214
- role="group"
215
- aria-label="Preview viewport"
216
- >
217
- {VIEWPORTS.map((v) => {
218
- const Icon = VIEWPORT_ICONS[v.icon] ?? Monitor
219
- const active = v.id === viewport
220
- return (
221
- <button
222
- key={v.id}
223
- type="button"
224
- onClick={() => onViewport(v.id)}
225
- aria-pressed={active}
226
- title={`${v.label} (${v.width}px)`}
227
- className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
228
- active
229
- ? 'bg-primary text-primary-foreground'
230
- : 'text-muted-foreground hover:text-foreground hover:bg-accent'
231
- }`}
232
- >
233
- <Icon className="h-3.5 w-3.5" aria-hidden />
234
- <span className="hidden lg:inline">{v.label}</span>
235
- </button>
236
- )
237
- })}
219
+ <div className="hidden items-center gap-1.5 md:flex">
220
+ <BreakpointPicker value={viewport} onChange={onViewport} />
221
+ {zoom !== undefined && onZoom && (
222
+ <ZoomChip zoom={zoom} scale={canvasScale ?? 1} onChange={onZoom} />
223
+ )}
238
224
  </div>
239
225
 
240
226
  <button
@@ -252,15 +238,21 @@ export function EditorTopBar({
252
238
  type="button"
253
239
  onClick={onOpenSeo}
254
240
  aria-pressed={seoOpen}
255
- aria-label={`SEO, score ${seoScore} out of 100`}
256
- title={`SEO — score ${seoScore}/100. Meta tags, social preview, schema, and analysis.`}
241
+ aria-label={seoScore === undefined ? 'SEO' : `SEO, score ${seoScore} out of 100`}
242
+ title={
243
+ seoScore === undefined
244
+ ? 'SEO — meta tags, social preview, schema, and analysis.'
245
+ : `SEO — score ${seoScore}/100. Meta tags, social preview, schema, and analysis.`
246
+ }
257
247
  className={`inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-medium transition-colors ${
258
248
  seoOpen
259
249
  ? 'bg-primary text-primary-foreground'
260
250
  : 'text-muted-foreground hover:text-foreground hover:bg-accent'
261
251
  }`}
262
252
  >
263
- <EditorSeoScoreBadge score={seoScore} inverted={seoOpen} />
253
+ {seoScore !== undefined && (
254
+ <EditorSeoScoreBadge score={seoScore} band={seoScoreBand} inverted={seoOpen} />
255
+ )}
264
256
  <span className="hidden sm:inline">SEO</span>
265
257
  </button>
266
258
  )}