@prenta/admin 0.92.0 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/design-system/tokens.css +173 -71
  3. package/design-system/tokens.json +244 -63
  4. package/design-system/tokens.scss +203 -98
  5. package/design-system/tokens.tailwind.cjs +93 -38
  6. package/dist/AdminRoot.d.ts +27 -6
  7. package/dist/AdminRoot.d.ts.map +1 -1
  8. package/dist/AdminRoot.js.map +1 -1
  9. package/dist/__tests__/components/breakpoint-controls.render.test.d.ts +2 -0
  10. package/dist/__tests__/components/breakpoint-controls.render.test.d.ts.map +1 -0
  11. package/dist/__tests__/components/breakpoint-controls.render.test.js +51 -0
  12. package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -0
  13. package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts +2 -0
  14. package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts.map +1 -0
  15. package/dist/__tests__/components/canvas-edit-tip.render.test.js +37 -0
  16. package/dist/__tests__/components/canvas-edit-tip.render.test.js.map +1 -0
  17. package/dist/__tests__/components/editor-rail.render.test.d.ts +2 -0
  18. package/dist/__tests__/components/editor-rail.render.test.d.ts.map +1 -0
  19. package/dist/__tests__/components/editor-rail.render.test.js +138 -0
  20. package/dist/__tests__/components/editor-rail.render.test.js.map +1 -0
  21. package/dist/__tests__/components/media-block.render.test.d.ts +2 -0
  22. package/dist/__tests__/components/media-block.render.test.d.ts.map +1 -0
  23. package/dist/__tests__/components/media-block.render.test.js +108 -0
  24. package/dist/__tests__/components/media-block.render.test.js.map +1 -0
  25. package/dist/__tests__/components/page-audit-panel.render.test.d.ts +2 -0
  26. package/dist/__tests__/components/page-audit-panel.render.test.d.ts.map +1 -0
  27. package/dist/__tests__/components/page-audit-panel.render.test.js +59 -0
  28. package/dist/__tests__/components/page-audit-panel.render.test.js.map +1 -0
  29. package/dist/__tests__/components/reflow-meter.render.test.d.ts +2 -0
  30. package/dist/__tests__/components/reflow-meter.render.test.d.ts.map +1 -0
  31. package/dist/__tests__/components/reflow-meter.render.test.js +94 -0
  32. package/dist/__tests__/components/reflow-meter.render.test.js.map +1 -0
  33. package/dist/__tests__/components/seo-char-count.test.js +24 -6
  34. package/dist/__tests__/components/seo-char-count.test.js.map +1 -1
  35. package/dist/__tests__/components/serp-preview.render.test.d.ts +2 -0
  36. package/dist/__tests__/components/serp-preview.render.test.d.ts.map +1 -0
  37. package/dist/__tests__/components/serp-preview.render.test.js +25 -0
  38. package/dist/__tests__/components/serp-preview.render.test.js.map +1 -0
  39. package/dist/__tests__/lib/breakpoints.test.d.ts +2 -0
  40. package/dist/__tests__/lib/breakpoints.test.d.ts.map +1 -0
  41. package/dist/__tests__/lib/breakpoints.test.js +67 -0
  42. package/dist/__tests__/lib/breakpoints.test.js.map +1 -0
  43. package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts +2 -0
  44. package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts.map +1 -0
  45. package/dist/__tests__/lib/canvas-edit-chrome.test.js +44 -0
  46. package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -0
  47. package/dist/__tests__/lib/media-format.test.d.ts +2 -0
  48. package/dist/__tests__/lib/media-format.test.d.ts.map +1 -0
  49. package/dist/__tests__/lib/media-format.test.js +70 -0
  50. package/dist/__tests__/lib/media-format.test.js.map +1 -0
  51. package/dist/__tests__/lib/page-audit-input.test.d.ts +2 -0
  52. package/dist/__tests__/lib/page-audit-input.test.d.ts.map +1 -0
  53. package/dist/__tests__/lib/page-audit-input.test.js +122 -0
  54. package/dist/__tests__/lib/page-audit-input.test.js.map +1 -0
  55. package/dist/__tests__/lib/page-editor-service.test.js +2 -0
  56. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
  57. package/dist/__tests__/lib/page-redirect.test.d.ts +2 -0
  58. package/dist/__tests__/lib/page-redirect.test.d.ts.map +1 -0
  59. package/dist/__tests__/lib/page-redirect.test.js +26 -0
  60. package/dist/__tests__/lib/page-redirect.test.js.map +1 -0
  61. package/dist/__tests__/lib/rail-width.test.d.ts +2 -0
  62. package/dist/__tests__/lib/rail-width.test.d.ts.map +1 -0
  63. package/dist/__tests__/lib/rail-width.test.js +69 -0
  64. package/dist/__tests__/lib/rail-width.test.js.map +1 -0
  65. package/dist/__tests__/lib/reflow-baseline.test.d.ts +2 -0
  66. package/dist/__tests__/lib/reflow-baseline.test.d.ts.map +1 -0
  67. package/dist/__tests__/lib/reflow-baseline.test.js +56 -0
  68. package/dist/__tests__/lib/reflow-baseline.test.js.map +1 -0
  69. package/dist/__tests__/lib/reflow-probe.test.d.ts +2 -0
  70. package/dist/__tests__/lib/reflow-probe.test.d.ts.map +1 -0
  71. package/dist/__tests__/lib/reflow-probe.test.js +73 -0
  72. package/dist/__tests__/lib/reflow-probe.test.js.map +1 -0
  73. package/dist/__tests__/lib/version-diff.test.d.ts +2 -0
  74. package/dist/__tests__/lib/version-diff.test.d.ts.map +1 -0
  75. package/dist/__tests__/lib/version-diff.test.js +230 -0
  76. package/dist/__tests__/lib/version-diff.test.js.map +1 -0
  77. package/dist/__tests__/lib/version-history.test.d.ts +2 -0
  78. package/dist/__tests__/lib/version-history.test.d.ts.map +1 -0
  79. package/dist/__tests__/lib/version-history.test.js +125 -0
  80. package/dist/__tests__/lib/version-history.test.js.map +1 -0
  81. package/dist/__tests__/styles/canvas-container-queries.test.d.ts +2 -0
  82. package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
  83. package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
  84. package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
  85. package/dist/__tests__/styles/packaging.test.d.ts +2 -0
  86. package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
  87. package/dist/__tests__/styles/packaging.test.js +31 -0
  88. package/dist/__tests__/styles/packaging.test.js.map +1 -0
  89. package/dist/__tests__/styles/responsive-layer.test.d.ts +2 -0
  90. package/dist/__tests__/styles/responsive-layer.test.d.ts.map +1 -0
  91. package/dist/__tests__/styles/responsive-layer.test.js +127 -0
  92. package/dist/__tests__/styles/responsive-layer.test.js.map +1 -0
  93. package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
  94. package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
  95. package/dist/__tests__/styles/tokens-drift.test.js +92 -0
  96. package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
  97. package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
  98. package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
  99. package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
  100. package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
  101. package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
  102. package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
  103. package/dist/__tests__/views/canvas-review-mode.render.test.d.ts +2 -0
  104. package/dist/__tests__/views/canvas-review-mode.render.test.d.ts.map +1 -0
  105. package/dist/__tests__/views/canvas-review-mode.render.test.js +70 -0
  106. package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -0
  107. package/dist/__tests__/views/editor-top-bar.render.test.js +30 -1
  108. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  109. package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
  110. package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
  111. package/dist/__tests__/views/page-tab.render.test.js +151 -0
  112. package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
  113. package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
  114. package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
  115. package/dist/__tests__/views/section-class-parity.test.js +81 -0
  116. package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
  117. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
  118. package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
  119. package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
  120. package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
  121. package/dist/__tests__/views/version-history-panel.render.test.d.ts +2 -0
  122. package/dist/__tests__/views/version-history-panel.render.test.d.ts.map +1 -0
  123. package/dist/__tests__/views/version-history-panel.render.test.js +153 -0
  124. package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -0
  125. package/dist/components/BreakpointPicker.d.ts +18 -0
  126. package/dist/components/BreakpointPicker.d.ts.map +1 -0
  127. package/dist/components/BreakpointPicker.js +31 -0
  128. package/dist/components/BreakpointPicker.js.map +1 -0
  129. package/dist/components/CanvasEditTip.d.ts +6 -0
  130. package/dist/components/CanvasEditTip.d.ts.map +1 -0
  131. package/dist/components/CanvasEditTip.js +22 -0
  132. package/dist/components/CanvasEditTip.js.map +1 -0
  133. package/dist/components/EditorRail.d.ts +36 -0
  134. package/dist/components/EditorRail.d.ts.map +1 -0
  135. package/dist/components/EditorRail.js +104 -0
  136. package/dist/components/EditorRail.js.map +1 -0
  137. package/dist/components/EditorRailTabs.d.ts +32 -0
  138. package/dist/components/EditorRailTabs.d.ts.map +1 -0
  139. package/dist/components/EditorRailTabs.js +45 -0
  140. package/dist/components/EditorRailTabs.js.map +1 -0
  141. package/dist/components/MediaBlock.d.ts +49 -0
  142. package/dist/components/MediaBlock.d.ts.map +1 -0
  143. package/dist/components/MediaBlock.js +50 -0
  144. package/dist/components/MediaBlock.js.map +1 -0
  145. package/dist/components/ReflowMeter.d.ts +28 -0
  146. package/dist/components/ReflowMeter.d.ts.map +1 -0
  147. package/dist/components/ReflowMeter.js +42 -0
  148. package/dist/components/ReflowMeter.js.map +1 -0
  149. package/dist/components/ZoomChip.d.ts +17 -0
  150. package/dist/components/ZoomChip.d.ts.map +1 -0
  151. package/dist/components/ZoomChip.js +17 -0
  152. package/dist/components/ZoomChip.js.map +1 -0
  153. package/dist/components/breakpoint-icons.d.ts +3 -0
  154. package/dist/components/breakpoint-icons.d.ts.map +1 -0
  155. package/dist/components/breakpoint-icons.js +16 -0
  156. package/dist/components/breakpoint-icons.js.map +1 -0
  157. package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
  158. package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
  159. package/dist/components/page-tab/AuthorPicker.js +95 -0
  160. package/dist/components/page-tab/AuthorPicker.js.map +1 -0
  161. package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
  162. package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
  163. package/dist/components/page-tab/DirectiveSwitch.js +33 -0
  164. package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
  165. package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
  166. package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
  167. package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
  168. package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
  169. package/dist/components/seo/PageAuditPanel.d.ts +14 -0
  170. package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
  171. package/dist/components/seo/PageAuditPanel.js +59 -0
  172. package/dist/components/seo/PageAuditPanel.js.map +1 -0
  173. package/dist/components/seo/SeoCharCount.d.ts +19 -7
  174. package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
  175. package/dist/components/seo/SeoCharCount.js +30 -16
  176. package/dist/components/seo/SeoCharCount.js.map +1 -1
  177. package/dist/components/seo/SeoEditorPane.d.ts +15 -1
  178. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  179. package/dist/components/seo/SeoEditorPane.js +118 -21
  180. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  181. package/dist/components/seo/SerpPreview.d.ts +20 -0
  182. package/dist/components/seo/SerpPreview.d.ts.map +1 -0
  183. package/dist/components/seo/SerpPreview.js +6 -0
  184. package/dist/components/seo/SerpPreview.js.map +1 -0
  185. package/dist/hooks/usePageAudit.d.ts +41 -0
  186. package/dist/hooks/usePageAudit.d.ts.map +1 -0
  187. package/dist/hooks/usePageAudit.js +68 -0
  188. package/dist/hooks/usePageAudit.js.map +1 -0
  189. package/dist/hooks/useReflowGuard.d.ts +47 -0
  190. package/dist/hooks/useReflowGuard.d.ts.map +1 -0
  191. package/dist/hooks/useReflowGuard.js +52 -0
  192. package/dist/hooks/useReflowGuard.js.map +1 -0
  193. package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
  194. package/dist/layout/primitives/AdminShell.js +2 -2
  195. package/dist/layout/primitives/AdminShell.js.map +1 -1
  196. package/dist/lib/breakpoints.d.ts +51 -0
  197. package/dist/lib/breakpoints.d.ts.map +1 -0
  198. package/dist/lib/breakpoints.js +84 -0
  199. package/dist/lib/breakpoints.js.map +1 -0
  200. package/dist/lib/canvas-edit-chrome.d.ts +22 -0
  201. package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
  202. package/dist/lib/canvas-edit-chrome.js +81 -0
  203. package/dist/lib/canvas-edit-chrome.js.map +1 -0
  204. package/dist/lib/media-format.d.ts +55 -0
  205. package/dist/lib/media-format.d.ts.map +1 -0
  206. package/dist/lib/media-format.js +93 -0
  207. package/dist/lib/media-format.js.map +1 -0
  208. package/dist/lib/page-audit-input.d.ts +45 -0
  209. package/dist/lib/page-audit-input.d.ts.map +1 -0
  210. package/dist/lib/page-audit-input.js +53 -0
  211. package/dist/lib/page-audit-input.js.map +1 -0
  212. package/dist/lib/page-editor-service.d.ts +12 -0
  213. package/dist/lib/page-editor-service.d.ts.map +1 -1
  214. package/dist/lib/page-editor-service.js +6 -0
  215. package/dist/lib/page-editor-service.js.map +1 -1
  216. package/dist/lib/page-redirect.d.ts +39 -0
  217. package/dist/lib/page-redirect.d.ts.map +1 -0
  218. package/dist/lib/page-redirect.js +38 -0
  219. package/dist/lib/page-redirect.js.map +1 -0
  220. package/dist/lib/rail-width.d.ts +23 -0
  221. package/dist/lib/rail-width.d.ts.map +1 -0
  222. package/dist/lib/rail-width.js +59 -0
  223. package/dist/lib/rail-width.js.map +1 -0
  224. package/dist/lib/reflow-baseline.d.ts +45 -0
  225. package/dist/lib/reflow-baseline.d.ts.map +1 -0
  226. package/dist/lib/reflow-baseline.js +38 -0
  227. package/dist/lib/reflow-baseline.js.map +1 -0
  228. package/dist/lib/reflow-probe.d.ts +159 -0
  229. package/dist/lib/reflow-probe.d.ts.map +1 -0
  230. package/dist/lib/reflow-probe.js +228 -0
  231. package/dist/lib/reflow-probe.js.map +1 -0
  232. package/dist/lib/seo-service.d.ts +10 -0
  233. package/dist/lib/seo-service.d.ts.map +1 -1
  234. package/dist/lib/seo-service.js.map +1 -1
  235. package/dist/lib/version-diff.d.ts +50 -0
  236. package/dist/lib/version-diff.d.ts.map +1 -0
  237. package/dist/lib/version-diff.js +294 -0
  238. package/dist/lib/version-diff.js.map +1 -0
  239. package/dist/lib/version-history.d.ts +73 -0
  240. package/dist/lib/version-history.d.ts.map +1 -0
  241. package/dist/lib/version-history.js +126 -0
  242. package/dist/lib/version-history.js.map +1 -0
  243. package/dist/prenta-admin.css +1 -1
  244. package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
  245. package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
  246. package/dist/views/page-editor/CanvasMetaLine.js +23 -0
  247. package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
  248. package/dist/views/page-editor/EditorCanvas.d.ts +47 -8
  249. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  250. package/dist/views/page-editor/EditorCanvas.js +68 -34
  251. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  252. package/dist/views/page-editor/EditorTopBar.d.ts +21 -2
  253. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  254. package/dist/views/page-editor/EditorTopBar.js +13 -20
  255. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  256. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  257. package/dist/views/page-editor/PageSectionEditor.js +383 -78
  258. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  259. package/dist/views/page-editor/PageTab.d.ts +36 -0
  260. package/dist/views/page-editor/PageTab.d.ts.map +1 -0
  261. package/dist/views/page-editor/PageTab.js +65 -0
  262. package/dist/views/page-editor/PageTab.js.map +1 -0
  263. package/dist/views/page-editor/ReviewBar.d.ts +21 -0
  264. package/dist/views/page-editor/ReviewBar.d.ts.map +1 -0
  265. package/dist/views/page-editor/ReviewBar.js +22 -0
  266. package/dist/views/page-editor/ReviewBar.js.map +1 -0
  267. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  268. package/dist/views/page-editor/SectionInspector.js +64 -20
  269. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  270. package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
  271. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
  272. package/dist/views/page-editor/SectionsPanel.js +38 -7
  273. package/dist/views/page-editor/SectionsPanel.js.map +1 -1
  274. package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
  275. package/dist/views/page-editor/SitePreviewFrame.js +3 -0
  276. package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
  277. package/dist/views/page-editor/VersionHistoryPanel.d.ts +29 -0
  278. package/dist/views/page-editor/VersionHistoryPanel.d.ts.map +1 -0
  279. package/dist/views/page-editor/VersionHistoryPanel.js +81 -0
  280. package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -0
  281. package/dist/views/page-editor/reflow-context.d.ts +28 -0
  282. package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
  283. package/dist/views/page-editor/reflow-context.js +14 -0
  284. package/dist/views/page-editor/reflow-context.js.map +1 -0
  285. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
  286. package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
  287. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
  288. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
  289. package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
  290. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
  291. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
  292. package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
  293. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
  294. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
  295. package/dist/views/page-editor/sections/GallerySection.js +7 -3
  296. package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
  297. package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
  298. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
  299. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
  300. package/dist/views/page-editor/sections/MissionSection.js +1 -1
  301. package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
  302. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
  303. package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
  304. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
  305. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
  306. package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
  307. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
  308. package/dist/views/page-editor/sections/SectionRenderer.d.ts +13 -1
  309. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  310. package/dist/views/page-editor/sections/SectionRenderer.js +66 -26
  311. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  312. package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
  313. package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
  314. package/dist/views/page-editor/sections/admin-renderers.js +27 -15
  315. package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
  316. package/dist/views/page-editor/sections/parts.d.ts +0 -1
  317. package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
  318. package/dist/views/page-editor/sections/parts.js +41 -16
  319. package/dist/views/page-editor/sections/parts.js.map +1 -1
  320. package/dist/views/page-editor/viewports.d.ts +7 -14
  321. package/dist/views/page-editor/viewports.d.ts.map +1 -1
  322. package/dist/views/page-editor/viewports.js +7 -12
  323. package/dist/views/page-editor/viewports.js.map +1 -1
  324. package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
  325. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  326. package/dist/views/post-editor/PostEditorCanvas.js +5 -2
  327. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  328. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  329. package/dist/views/post-editor/PostSectionEditor.js +2 -20
  330. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  331. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
  332. package/dist/views/post-editor/PostTemplateEditor.js +4 -1
  333. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
  334. package/package.json +5 -3
  335. package/src/AdminRoot.tsx +27 -6
  336. package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
  337. package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
  338. package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
  339. package/src/__tests__/components/media-block.render.test.tsx +126 -0
  340. package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
  341. package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
  342. package/src/__tests__/components/seo-char-count.test.tsx +26 -6
  343. package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
  344. package/src/__tests__/lib/breakpoints.test.ts +84 -0
  345. package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
  346. package/src/__tests__/lib/media-format.test.ts +89 -0
  347. package/src/__tests__/lib/page-audit-input.test.ts +144 -0
  348. package/src/__tests__/lib/page-editor-service.test.ts +2 -0
  349. package/src/__tests__/lib/page-redirect.test.ts +29 -0
  350. package/src/__tests__/lib/rail-width.test.ts +86 -0
  351. package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
  352. package/src/__tests__/lib/reflow-probe.test.ts +95 -0
  353. package/src/__tests__/lib/version-diff.test.ts +278 -0
  354. package/src/__tests__/lib/version-history.test.ts +166 -0
  355. package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
  356. package/src/__tests__/styles/packaging.test.ts +38 -0
  357. package/src/__tests__/styles/responsive-layer.test.ts +152 -0
  358. package/src/__tests__/styles/tokens-drift.test.ts +110 -0
  359. package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
  360. package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
  361. package/src/__tests__/views/canvas-review-mode.render.test.tsx +94 -0
  362. package/src/__tests__/views/editor-top-bar.render.test.tsx +43 -1
  363. package/src/__tests__/views/page-tab.render.test.tsx +226 -0
  364. package/src/__tests__/views/section-class-parity.test.ts +98 -0
  365. package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
  366. package/src/__tests__/views/version-history-panel.render.test.tsx +195 -0
  367. package/src/components/BreakpointPicker.tsx +114 -0
  368. package/src/components/CanvasEditTip.tsx +46 -0
  369. package/src/components/EditorRail.tsx +219 -0
  370. package/src/components/EditorRailTabs.tsx +117 -0
  371. package/src/components/MediaBlock.tsx +223 -0
  372. package/src/components/ReflowMeter.tsx +125 -0
  373. package/src/components/ZoomChip.tsx +39 -0
  374. package/src/components/breakpoint-icons.ts +17 -0
  375. package/src/components/page-tab/AuthorPicker.tsx +240 -0
  376. package/src/components/page-tab/DirectiveSwitch.tsx +89 -0
  377. package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
  378. package/src/components/seo/PageAuditPanel.tsx +192 -0
  379. package/src/components/seo/SeoCharCount.tsx +55 -24
  380. package/src/components/seo/SeoEditorPane.tsx +213 -31
  381. package/src/components/seo/SerpPreview.tsx +41 -0
  382. package/src/hooks/usePageAudit.ts +98 -0
  383. package/src/hooks/useReflowGuard.ts +96 -0
  384. package/src/layout/primitives/AdminShell.tsx +13 -1
  385. package/src/lib/breakpoints.ts +107 -0
  386. package/src/lib/canvas-edit-chrome.ts +82 -0
  387. package/src/lib/media-format.ts +99 -0
  388. package/src/lib/page-audit-input.ts +104 -0
  389. package/src/lib/page-editor-service.ts +18 -0
  390. package/src/lib/page-redirect.ts +60 -0
  391. package/src/lib/rail-width.ts +58 -0
  392. package/src/lib/reflow-baseline.ts +70 -0
  393. package/src/lib/reflow-probe.ts +299 -0
  394. package/src/lib/seo-service.ts +10 -0
  395. package/src/lib/version-diff.ts +376 -0
  396. package/src/lib/version-history.ts +192 -0
  397. package/src/styles/prenta-tokens.css +120 -42
  398. package/src/styles/responsive.css +281 -0
  399. package/src/styles/theme.css +138 -0
  400. package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
  401. package/src/views/page-editor/EditorCanvas.tsx +140 -54
  402. package/src/views/page-editor/EditorTopBar.tsx +55 -51
  403. package/src/views/page-editor/PageSectionEditor.tsx +575 -185
  404. package/src/views/page-editor/PageTab.tsx +433 -0
  405. package/src/views/page-editor/ReviewBar.tsx +76 -0
  406. package/src/views/page-editor/SectionInspector.tsx +84 -42
  407. package/src/views/page-editor/SectionsPanel.tsx +282 -141
  408. package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
  409. package/src/views/page-editor/VersionHistoryPanel.tsx +302 -0
  410. package/src/views/page-editor/reflow-context.ts +43 -0
  411. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
  412. package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
  413. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
  414. package/src/views/page-editor/sections/GallerySection.tsx +7 -3
  415. package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
  416. package/src/views/page-editor/sections/MissionSection.tsx +4 -2
  417. package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
  418. package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
  419. package/src/views/page-editor/sections/SectionRenderer.tsx +115 -34
  420. package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
  421. package/src/views/page-editor/sections/parts.tsx +46 -18
  422. package/src/views/page-editor/viewports.ts +13 -26
  423. package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
  424. package/src/views/post-editor/PostSectionEditor.tsx +1 -56
  425. package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
@@ -0,0 +1,223 @@
1
+ 'use client'
2
+
3
+ import { useState, type ReactNode } from 'react'
4
+ import { Check, ImageIcon, Trash2 } from 'lucide-react'
5
+ import { useApiData } from '../lib/useApiData.js'
6
+ import {
7
+ CROP_RATIO,
8
+ assetWash,
9
+ displayName,
10
+ formatBadge,
11
+ ratioLabel,
12
+ type MediaCrop,
13
+ } from '../lib/media-format.js'
14
+
15
+ interface MediaItem {
16
+ id: string
17
+ filename: string
18
+ storageKey: string
19
+ url?: string
20
+ altTag?: string
21
+ mimeType: string
22
+ width?: number
23
+ height?: number
24
+ }
25
+
26
+ export interface MediaBlockProps {
27
+ /** Current asset URL, or empty for the empty state. */
28
+ value: string
29
+ onChange: (url: string, item?: MediaItem) => void
30
+ /** The crop the template renders this image at. */
31
+ crop?: MediaCrop
32
+ /** Rows rendered between the frame and the toolbar (the social preview). */
33
+ children?: ReactNode
34
+ disabled?: boolean
35
+ /** Accessible label, e.g. "Hero image". */
36
+ label?: string
37
+ }
38
+
39
+ /**
40
+ * One component for every image — section slots and the social/OG card alike.
41
+ *
42
+ * ── The frame shows the TEMPLATE's crop, not the asset's ─────────────────
43
+ *
44
+ * A section image renders at 16:10 and a social card at 1.91:1 regardless of
45
+ * what was uploaded. Framing the asset's own ratio would tell an editor their
46
+ * portrait photo looks fine and then crop its subject out on the page. What
47
+ * the asset actually is gets reported in the toolbar instead, as a ratio label
48
+ * beside its filename, so both facts are visible and neither is implied.
49
+ *
50
+ * ── The toolbar is always visible ────────────────────────────────────────
51
+ *
52
+ * Never hover-only. Replace and Remove are the two things an editor comes here
53
+ * to do, and hiding them behind a hover makes them undiscoverable on touch and
54
+ * invisible to anyone scanning the panel.
55
+ *
56
+ * ── The picker opens inside the shell ────────────────────────────────────
57
+ *
58
+ * Not a modal. Choosing an image is a comparison against the slot it is going
59
+ * into, and a dialog covering the slot removes the only context that matters.
60
+ */
61
+ export function MediaBlock({
62
+ value,
63
+ onChange,
64
+ crop = 'section',
65
+ children,
66
+ disabled = false,
67
+ label = 'Image',
68
+ }: MediaBlockProps) {
69
+ const [picking, setPicking] = useState(false)
70
+ const { data } = useApiData<{ items?: MediaItem[] }>(picking ? '/media?pageSize=50' : null)
71
+ const items = (Array.isArray(data) ? data : (data?.items ?? [])).filter((i: MediaItem) =>
72
+ i.mimeType?.startsWith('image/'),
73
+ )
74
+
75
+ const ratio = CROP_RATIO[crop]
76
+ const name = value ? displayName(value) : ''
77
+
78
+ return (
79
+ <div className="border-border bg-card overflow-hidden rounded-[10px] border">
80
+ {value ? (
81
+ <div
82
+ className="w-full bg-cover bg-center"
83
+ style={{ aspectRatio: ratio, backgroundImage: assetWash(name) }}
84
+ >
85
+ <img
86
+ src={value}
87
+ alt=""
88
+ className="h-full w-full object-cover"
89
+ style={{ aspectRatio: ratio }}
90
+ />
91
+ </div>
92
+ ) : (
93
+ <button
94
+ type="button"
95
+ onClick={() => setPicking(true)}
96
+ disabled={disabled}
97
+ aria-label={`Choose ${label.toLowerCase()}`}
98
+ className="bg-muted hover:bg-accent group flex w-full items-center justify-center p-3 transition-colors disabled:opacity-50"
99
+ style={{ aspectRatio: ratio }}
100
+ >
101
+ <span className="border-border group-hover:border-primary group-hover:text-primary text-muted-foreground flex h-full w-full flex-col items-center justify-center gap-1.5 rounded-lg border-[1.5px] border-dashed transition-colors">
102
+ <ImageIcon size={22} aria-hidden />
103
+ <span className="text-foreground text-[12.5px] font-semibold">No image selected</span>
104
+ <span className="text-[10.5px]">Choose one from the library</span>
105
+ </span>
106
+ </button>
107
+ )}
108
+
109
+ {/* The social block puts its preview rows here — between the frame and
110
+ the toolbar — so the share card and its upload target read as one
111
+ object rather than two stacked controls. */}
112
+ {children}
113
+
114
+ <div className="border-border flex items-center gap-2 border-t px-2.5 py-2">
115
+ {value ? (
116
+ <>
117
+ <span
118
+ className="text-foreground min-w-0 flex-1 truncate text-[11.5px] font-semibold"
119
+ title={name}
120
+ >
121
+ {name}
122
+ </span>
123
+ <span className="text-muted-foreground shrink-0 text-[10.5px] tabular-nums">
124
+ {formatBadge(value)}
125
+ </span>
126
+ <button
127
+ type="button"
128
+ onClick={() => setPicking((v) => !v)}
129
+ disabled={disabled}
130
+ className="border-border hover:bg-accent shrink-0 rounded-md border px-2 py-1 text-[11px] font-medium disabled:opacity-50"
131
+ >
132
+ Replace
133
+ </button>
134
+ <button
135
+ type="button"
136
+ onClick={() => onChange('')}
137
+ disabled={disabled}
138
+ aria-label={`Remove ${label.toLowerCase()}`}
139
+ className="text-muted-foreground hover:text-destructive shrink-0 rounded p-1 disabled:opacity-50"
140
+ >
141
+ <Trash2 size={13} aria-hidden />
142
+ </button>
143
+ </>
144
+ ) : (
145
+ <>
146
+ <span className="text-muted-foreground min-w-0 flex-1 text-[11.5px]">
147
+ No image selected
148
+ </span>
149
+ <button
150
+ type="button"
151
+ onClick={() => setPicking((v) => !v)}
152
+ disabled={disabled}
153
+ className="border-border hover:bg-accent shrink-0 rounded-md border px-2 py-1 text-[11px] font-medium disabled:opacity-50"
154
+ >
155
+ Choose
156
+ </button>
157
+ </>
158
+ )}
159
+ </div>
160
+
161
+ {picking && (
162
+ <div className="border-border bg-muted max-h-[262px] overflow-y-auto border-t p-2">
163
+ {items.length === 0 ? (
164
+ <p className="text-muted-foreground px-1 py-3 text-center text-[11px]">
165
+ No images in the library yet.
166
+ </p>
167
+ ) : (
168
+ <ul className="grid grid-cols-2 gap-2">
169
+ {items.map((item) => {
170
+ const url = item.url || item.storageKey
171
+ const selected = url === value
172
+ const itemRatio = ratioLabel(item.width, item.height)
173
+ return (
174
+ <li key={item.id}>
175
+ <button
176
+ type="button"
177
+ onClick={() => {
178
+ onChange(url, item)
179
+ setPicking(false)
180
+ }}
181
+ aria-current={selected || undefined}
182
+ className={`border-border bg-card w-full overflow-hidden rounded-lg border text-left transition-colors ${
183
+ selected ? 'border-primary' : 'hover:border-ring'
184
+ }`}
185
+ >
186
+ <span
187
+ className="relative block w-full"
188
+ style={{
189
+ aspectRatio: '16 / 10',
190
+ backgroundImage: assetWash(item.filename),
191
+ }}
192
+ >
193
+ {url && <img src={url} alt="" className="h-full w-full object-cover" />}
194
+ <span className="absolute top-1 left-1 rounded-[5px] bg-white/80 px-1.5 py-px text-[9.5px] font-extrabold tracking-wide text-zinc-800">
195
+ {formatBadge(item.filename, item.mimeType)}
196
+ </span>
197
+ {selected && (
198
+ <span className="bg-primary text-primary-foreground absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full">
199
+ <Check size={10} aria-hidden />
200
+ </span>
201
+ )}
202
+ </span>
203
+ <span className="block px-1.5 py-1">
204
+ <span className="text-foreground block truncate text-[10px] font-semibold">
205
+ {item.filename}
206
+ </span>
207
+ <span className="text-muted-foreground block text-[9px] tabular-nums">
208
+ {item.width && item.height
209
+ ? `${item.width} × ${item.height}${itemRatio ? ` · ${itemRatio}` : ''}`
210
+ : 'Dimensions unknown'}
211
+ </span>
212
+ </span>
213
+ </button>
214
+ </li>
215
+ )
216
+ })}
217
+ </ul>
218
+ )}
219
+ </div>
220
+ )}
221
+ </div>
222
+ )
223
+ }
@@ -0,0 +1,125 @@
1
+ 'use client'
2
+
3
+ import { AlertTriangle } from 'lucide-react'
4
+ import { BREAKPOINTS, getBreakpoint, type BreakpointId } from '../lib/breakpoints.js'
5
+ import type { ReflowGuardResult } from '../hooks/useReflowGuard.js'
6
+
7
+ export interface ReflowMeterProps {
8
+ guard: ReflowGuardResult
9
+ /** Character count of the current value. */
10
+ length: number
11
+ /** Breakpoint the canvas is currently showing — marked in the row of dots. */
12
+ device: BreakpointId
13
+ /** Switch the canvas to a breakpoint so the editor can look at the damage. */
14
+ onProof: (id: BreakpointId) => void
15
+ }
16
+
17
+ const DOT_TONE: Record<string, string> = {
18
+ same: 'bg-success/12 text-success',
19
+ grew: 'bg-warning/15 text-warning',
20
+ 'grew-badly': 'bg-destructive/12 text-destructive',
21
+ }
22
+
23
+ /**
24
+ * Per-field reflow readout: character count, line count where you're looking,
25
+ * one dot per breakpoint, and a proof prompt when wrapping changed.
26
+ *
27
+ * ── Why the dots are buttons ──────────────────────────────────────────────
28
+ *
29
+ * The guard's whole value is that it reports damage at a width you are NOT
30
+ * looking at. Telling an editor "this now runs three lines on mobile" while
31
+ * making them hunt for the breakpoint switcher wastes the finding. Each dot
32
+ * takes the canvas there.
33
+ *
34
+ * Colour is never the only signal: every dot carries its breakpoint's initials
35
+ * and an accessible name stating the line count and what it was published at,
36
+ * and the alert below repeats the finding in words.
37
+ */
38
+ export function ReflowMeter({ guard, length, device, onProof }: ReflowMeterProps) {
39
+ const here = guard.rows.find((r) => r.id === device)
40
+ const worst = guard.worst ? guard.rows.find((r) => r.id === guard.worst) : undefined
41
+
42
+ // Named breakpoints while the list is short enough to read; initials once it
43
+ // isn't; "every breakpoint" when it's all of them, which is shorter and is
44
+ // also the more useful statement.
45
+ const grown = guard.grown
46
+ const scope =
47
+ grown.length === BREAKPOINTS.length
48
+ ? 'every breakpoint'
49
+ : grown.length > 3
50
+ ? grown.map((r) => getBreakpoint(r.id).abbr).join(', ')
51
+ : grown.map((r) => getBreakpoint(r.id).name).join(', ')
52
+
53
+ return (
54
+ <>
55
+ <div className="mt-1.5 flex items-center gap-2">
56
+ <span className="text-muted-foreground text-[11px] whitespace-nowrap tabular-nums">
57
+ <b className="text-foreground/80 font-semibold">{length}</b> chars
58
+ {guard.delta !== 0 && (
59
+ <span className={guard.delta > 0 ? 'text-warning font-bold' : ''}>
60
+ {' '}
61
+ ({guard.delta > 0 ? '+' : ''}
62
+ {guard.delta})
63
+ </span>
64
+ )}
65
+ {here && (
66
+ <>
67
+ {' · '}
68
+ <b className="text-foreground/80 font-semibold">{here.lines}</b> line
69
+ {here.lines === 1 ? '' : 's'} here
70
+ </>
71
+ )}
72
+ </span>
73
+
74
+ {guard.rows.length > 0 && (
75
+ <div className="ml-auto flex gap-0.75">
76
+ {guard.rows.map((row) => {
77
+ const bp = getBreakpoint(row.id)
78
+ return (
79
+ <button
80
+ key={row.id}
81
+ type="button"
82
+ onClick={() => onProof(row.id)}
83
+ className={`flex h-4.25 w-4.25 items-center justify-center rounded-[5px] text-[8px] font-bold transition-transform hover:-translate-y-px ${
84
+ DOT_TONE[row.state] ?? 'bg-muted text-muted-foreground'
85
+ } ${device === row.id ? 'outline-primary outline-[1.5px] outline-offset-1' : ''}`}
86
+ title={`${bp.name} — ${row.lines} line${row.lines === 1 ? '' : 's'}${
87
+ row.diff > 0 ? ` (published: ${row.was}) — wrapping changed` : ''
88
+ }. Click to proof.`}
89
+ >
90
+ <span aria-hidden>{bp.abbr}</span>
91
+ {/* Leads with the breakpoint and its finding, not with the
92
+ action. The alert below carries a "Proof on ‹bp›" button,
93
+ and two controls with the same accessible name in one
94
+ region are indistinguishable to a screen reader. */}
95
+ <span className="sr-only">
96
+ {bp.name}: {row.lines} line{row.lines === 1 ? '' : 's'}
97
+ {row.diff > 0 ? `, was ${row.was} when published` : ''} — proof here
98
+ </span>
99
+ </button>
100
+ )
101
+ })}
102
+ </div>
103
+ )}
104
+ </div>
105
+
106
+ {worst && (
107
+ <div className="bg-warning/10 border-warning/30 mt-1.5 grid grid-cols-[auto_1fr] gap-x-2.5 gap-y-1 rounded-lg border p-2.5">
108
+ <AlertTriangle size={14} className="text-warning mt-px shrink-0" aria-hidden />
109
+ <p className="text-[11.5px] leading-normal">
110
+ Wrapping changed on <b className="text-warning font-bold">{scope}</b> —{' '}
111
+ {getBreakpoint(worst.id).name} now runs {worst.lines} lines instead of {worst.was}.
112
+ Proof it before publishing.
113
+ </p>
114
+ <button
115
+ type="button"
116
+ onClick={() => onProof(worst.id)}
117
+ className="border-warning text-warning hover:bg-warning/15 col-start-2 mt-0.5 justify-self-start rounded-md border px-2.5 py-1 text-[11px] font-semibold whitespace-nowrap transition-colors"
118
+ >
119
+ Proof on {getBreakpoint(worst.id).name}
120
+ </button>
121
+ </div>
122
+ )}
123
+ </>
124
+ )
125
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import type { Zoom } from '../lib/breakpoints.js'
4
+
5
+ export interface ZoomChipProps {
6
+ /** Current zoom mode. */
7
+ zoom: Zoom
8
+ /** Actual rendered scale, 0–1 — what the canvas measured, not the mode. */
9
+ scale: number
10
+ onChange: (zoom: Zoom) => void
11
+ }
12
+
13
+ /**
14
+ * Reads out the canvas scale and toggles fit ↔ 100%.
15
+ *
16
+ * It shows the *measured* scale rather than the mode, because "fit" is not a
17
+ * number an editor can reason about — at 1920px in a narrow rail it might be
18
+ * 38%, and that is the fact that explains why the preview looks small.
19
+ */
20
+ export function ZoomChip({ zoom, scale, onChange }: ZoomChipProps) {
21
+ const percent = Math.round(scale * 100)
22
+ const next: Zoom = zoom === 'fit' ? 1 : 'fit'
23
+
24
+ return (
25
+ <button
26
+ type="button"
27
+ onClick={() => onChange(next)}
28
+ aria-label={
29
+ zoom === 'fit'
30
+ ? `Canvas scaled to ${percent} percent to fit. Switch to actual size.`
31
+ : 'Canvas at actual size. Switch to fit.'
32
+ }
33
+ title={zoom === 'fit' ? 'Fit to window — click for 100%' : 'Actual size — click to fit'}
34
+ className="border-border bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground h-[34px] min-w-[54px] rounded-lg border px-2 text-[11.5px] font-semibold tabular-nums transition-colors"
35
+ >
36
+ {percent}%
37
+ </button>
38
+ )
39
+ }
@@ -0,0 +1,17 @@
1
+ import { Laptop, Monitor, Smartphone, Tablet, type LucideIcon } from 'lucide-react'
2
+
3
+ /**
4
+ * Maps a breakpoint's `icon` name to its component. Kept out of
5
+ * `lib/breakpoints.ts` so that module stays free of React imports — it is pure
6
+ * data and arithmetic, and its tests run without a DOM.
7
+ */
8
+ const ICONS: Record<string, LucideIcon> = {
9
+ monitor: Monitor,
10
+ laptop: Laptop,
11
+ tablet: Tablet,
12
+ smartphone: Smartphone,
13
+ }
14
+
15
+ export function breakpointIcon(name: string): LucideIcon {
16
+ return ICONS[name] ?? Monitor
17
+ }
@@ -0,0 +1,240 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { ChevronDown, Globe } from 'lucide-react'
5
+ import { cmsApi } from '../../lib/api.js'
6
+
7
+ interface CmsUser {
8
+ id: string
9
+ name?: string | null
10
+ email?: string | null
11
+ role?: string | null
12
+ }
13
+
14
+ export interface AuthorValue {
15
+ /** Displayed byline. Empty means no byline is set. */
16
+ name: string
17
+ /** CMS user id, when the byline is one. Empty for generic and guest. */
18
+ id: string
19
+ }
20
+
21
+ export interface AuthorPickerProps {
22
+ value: AuthorValue
23
+ onChange: (next: AuthorValue) => void
24
+ /** Brand byline offered under GENERIC — usually the site or org name. */
25
+ brandName?: string
26
+ disabled?: boolean
27
+ }
28
+
29
+ /**
30
+ * Byline picker: a CMS user, the brand, or a one-off guest name.
31
+ *
32
+ * ── What this does and does not change ────────────────────────────────────
33
+ *
34
+ * It sets the page's DISPLAYED author, which reaches the page's structured
35
+ * data. It does not reassign the document's `createdById` — that records who
36
+ * created the record, and trading an audit trail for a label is not a swap
37
+ * worth making silently. A page written by a staff editor and bylined to the
38
+ * brand is a normal thing; a page whose history says someone else made it is
39
+ * not.
40
+ *
41
+ * The menu renders inline below the trigger rather than floating. The rail is
42
+ * narrow and scrollable, and a floating menu inside a scroll container either
43
+ * clips or detaches from its trigger on scroll.
44
+ */
45
+ export function AuthorPicker({ value, onChange, brandName, disabled = false }: AuthorPickerProps) {
46
+ const [open, setOpen] = useState(false)
47
+ const [users, setUsers] = useState<CmsUser[]>([])
48
+ const [guest, setGuest] = useState('')
49
+
50
+ useEffect(() => {
51
+ if (!open || users.length > 0) return
52
+ let cancelled = false
53
+ cmsApi<CmsUser[] | { users?: CmsUser[] }>('/users').then((res) => {
54
+ if (cancelled || !res.data) return
55
+ const list = Array.isArray(res.data) ? res.data : (res.data.users ?? [])
56
+ setUsers(list)
57
+ })
58
+ return () => {
59
+ cancelled = true
60
+ }
61
+ }, [open, users.length])
62
+
63
+ const isGeneric = Boolean(brandName) && value.name === brandName && !value.id
64
+ const label = value.name || 'No byline'
65
+ const sub = value.id
66
+ ? (users.find((u) => u.id === value.id)?.role ?? 'CMS user')
67
+ : isGeneric
68
+ ? 'Brand byline'
69
+ : value.name
70
+ ? 'Guest byline'
71
+ : 'None set'
72
+
73
+ function pick(next: AuthorValue) {
74
+ onChange(next)
75
+ setOpen(false)
76
+ }
77
+
78
+ return (
79
+ <div>
80
+ <button
81
+ type="button"
82
+ onClick={() => setOpen((v) => !v)}
83
+ disabled={disabled}
84
+ aria-expanded={open}
85
+ className="border-border bg-card hover:border-ring flex w-full items-center gap-2.5 rounded-[7px] border-[1.5px] px-2.5 py-[7px] text-left transition-colors disabled:opacity-50"
86
+ >
87
+ <Avatar name={value.name} generic={isGeneric} />
88
+ <span className="min-w-0 flex-1">
89
+ <span className="text-foreground block truncate text-[12.5px] font-semibold">
90
+ {label}
91
+ </span>
92
+ <span className="text-muted-foreground block text-[10.5px]">{sub}</span>
93
+ </span>
94
+ <ChevronDown
95
+ size={14}
96
+ aria-hidden
97
+ className={`text-muted-foreground shrink-0 motion-safe:transition-transform ${open ? 'rotate-180' : ''}`}
98
+ />
99
+ </button>
100
+
101
+ {open && (
102
+ <div className="border-border mt-1.5 space-y-2 rounded-[9px] border p-2">
103
+ <Group label="CMS users">
104
+ {users.length === 0 ? (
105
+ <p className="text-muted-foreground px-1 py-1.5 text-[11px]">Loading…</p>
106
+ ) : (
107
+ users.map((u) => {
108
+ const name = u.name || u.email || 'Unnamed user'
109
+ return (
110
+ <Row
111
+ key={u.id}
112
+ selected={value.id === u.id}
113
+ onClick={() => pick({ name, id: u.id })}
114
+ >
115
+ <Avatar name={name} />
116
+ <span className="min-w-0">
117
+ <span className="block truncate text-[12px] font-medium">{name}</span>
118
+ <span className="text-muted-foreground block text-[10px]">
119
+ {u.role ?? 'Author'}
120
+ </span>
121
+ </span>
122
+ </Row>
123
+ )
124
+ })
125
+ )}
126
+ </Group>
127
+
128
+ {brandName && (
129
+ <Group label="Generic">
130
+ <Row selected={isGeneric} onClick={() => pick({ name: brandName, id: '' })}>
131
+ <Avatar name={brandName} generic />
132
+ <span className="text-[12px] font-medium">{brandName}</span>
133
+ </Row>
134
+ </Group>
135
+ )}
136
+
137
+ <Group label="Guest byline">
138
+ <div className="flex gap-1.5 px-1">
139
+ <input
140
+ value={guest}
141
+ onChange={(e) => setGuest(e.target.value)}
142
+ placeholder="Name to display"
143
+ aria-label="Guest byline name"
144
+ className="border-border bg-input-background focus-visible:border-ring min-w-0 flex-1 rounded-md border px-2 py-1 text-[11.5px] focus-visible:outline-none"
145
+ />
146
+ <button
147
+ type="button"
148
+ // Disabled until non-empty: an empty guest byline would clear
149
+ // the author while looking like it set one.
150
+ disabled={guest.trim().length === 0}
151
+ onClick={() => {
152
+ pick({ name: guest.trim(), id: '' })
153
+ setGuest('')
154
+ }}
155
+ className="bg-primary text-primary-foreground rounded-md px-2 py-1 text-[11px] font-semibold disabled:opacity-40"
156
+ >
157
+ Add
158
+ </button>
159
+ </div>
160
+ </Group>
161
+ </div>
162
+ )}
163
+ </div>
164
+ )
165
+ }
166
+
167
+ function Group({ label, children }: { label: string; children: React.ReactNode }) {
168
+ return (
169
+ <div>
170
+ <p className="text-muted-foreground px-1 pb-1 text-[9.5px] font-bold tracking-wide uppercase">
171
+ {label}
172
+ </p>
173
+ <div className="space-y-0.5">{children}</div>
174
+ </div>
175
+ )
176
+ }
177
+
178
+ function Row({
179
+ selected,
180
+ onClick,
181
+ children,
182
+ }: {
183
+ selected: boolean
184
+ onClick: () => void
185
+ children: React.ReactNode
186
+ }) {
187
+ return (
188
+ <button
189
+ type="button"
190
+ onClick={onClick}
191
+ aria-current={selected || undefined}
192
+ className={`flex w-full items-center gap-2 rounded-md px-1.5 py-1.5 text-left transition-colors ${
193
+ selected ? 'bg-accent' : 'hover:bg-accent/60'
194
+ }`}
195
+ >
196
+ {children}
197
+ </button>
198
+ )
199
+ }
200
+
201
+ /**
202
+ * Initials on a tint derived from the name, so the same person is the same
203
+ * colour everywhere without storing one. A globe replaces initials for the
204
+ * brand byline — "Ex" for "Example Inc" reads as a person, which is the one
205
+ * thing the generic option is not.
206
+ */
207
+ function Avatar({ name, generic = false }: { name: string; generic?: boolean }) {
208
+ if (generic) {
209
+ return (
210
+ <span className="bg-muted text-muted-foreground flex h-[27px] w-[27px] shrink-0 items-center justify-center rounded-full">
211
+ <Globe size={14} aria-hidden />
212
+ </span>
213
+ )
214
+ }
215
+
216
+ const initials =
217
+ name
218
+ .trim()
219
+ .split(/\s+/)
220
+ .slice(0, 2)
221
+ .map((w) => w[0] ?? '')
222
+ .join('')
223
+ .toUpperCase() || '—'
224
+
225
+ let hash = 0
226
+ for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) % 360
227
+
228
+ return (
229
+ <span
230
+ aria-hidden
231
+ className="flex h-[27px] w-[27px] shrink-0 items-center justify-center rounded-full text-[10px] font-bold"
232
+ style={{
233
+ backgroundColor: `oklch(0.92 0.05 ${hash})`,
234
+ color: `oklch(0.42 0.12 ${hash})`,
235
+ }}
236
+ >
237
+ {initials}
238
+ </span>
239
+ )
240
+ }