@actuate-media/cms-admin 0.14.1 → 0.16.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 (304) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +148 -2
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
  5. package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/posts-badges.test.js +68 -0
  7. package/dist/__tests__/components/posts-badges.test.js.map +1 -0
  8. package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
  9. package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
  10. package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
  11. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
  12. package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
  13. package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
  14. package/dist/__tests__/lib/page-editor-service.test.js +137 -0
  15. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
  16. package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
  17. package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
  18. package/dist/__tests__/lib/pages-service.test.js +140 -0
  19. package/dist/__tests__/lib/pages-service.test.js.map +1 -0
  20. package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
  21. package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
  22. package/dist/__tests__/lib/post-editor-service.test.js +185 -0
  23. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
  24. package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
  25. package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
  26. package/dist/__tests__/lib/posts-service.test.js +244 -0
  27. package/dist/__tests__/lib/posts-service.test.js.map +1 -0
  28. package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
  29. package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
  30. package/dist/__tests__/views/posts-list-view.test.js +98 -0
  31. package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
  32. package/dist/actuate-admin.css +1 -1
  33. package/dist/components/pages/tag-badges.d.ts +21 -0
  34. package/dist/components/pages/tag-badges.d.ts.map +1 -0
  35. package/dist/components/pages/tag-badges.js +56 -0
  36. package/dist/components/pages/tag-badges.js.map +1 -0
  37. package/dist/components/posts/badges.d.ts +25 -0
  38. package/dist/components/posts/badges.d.ts.map +1 -0
  39. package/dist/components/posts/badges.js +123 -0
  40. package/dist/components/posts/badges.js.map +1 -0
  41. package/dist/components/posts/icons.d.ts +12 -0
  42. package/dist/components/posts/icons.d.ts.map +1 -0
  43. package/dist/components/posts/icons.js +26 -0
  44. package/dist/components/posts/icons.js.map +1 -0
  45. package/dist/layout/Header.d.ts +14 -1
  46. package/dist/layout/Header.d.ts.map +1 -1
  47. package/dist/layout/Header.js +10 -3
  48. package/dist/layout/Header.js.map +1 -1
  49. package/dist/layout/Layout.d.ts.map +1 -1
  50. package/dist/layout/Layout.js +78 -2
  51. package/dist/layout/Layout.js.map +1 -1
  52. package/dist/layout/Sidebar.d.ts +16 -0
  53. package/dist/layout/Sidebar.d.ts.map +1 -1
  54. package/dist/layout/Sidebar.js +176 -49
  55. package/dist/layout/Sidebar.js.map +1 -1
  56. package/dist/lib/page-editor-service.d.ts +45 -0
  57. package/dist/lib/page-editor-service.d.ts.map +1 -0
  58. package/dist/lib/page-editor-service.js +153 -0
  59. package/dist/lib/page-editor-service.js.map +1 -0
  60. package/dist/lib/pages-service.d.ts +143 -0
  61. package/dist/lib/pages-service.d.ts.map +1 -0
  62. package/dist/lib/pages-service.js +444 -0
  63. package/dist/lib/pages-service.js.map +1 -0
  64. package/dist/lib/post-editor-service.d.ts +57 -0
  65. package/dist/lib/post-editor-service.d.ts.map +1 -0
  66. package/dist/lib/post-editor-service.js +234 -0
  67. package/dist/lib/post-editor-service.js.map +1 -0
  68. package/dist/lib/posts-service.d.ts +73 -0
  69. package/dist/lib/posts-service.d.ts.map +1 -0
  70. package/dist/lib/posts-service.js +245 -0
  71. package/dist/lib/posts-service.js.map +1 -0
  72. package/dist/lib/preview-link.d.ts +32 -0
  73. package/dist/lib/preview-link.d.ts.map +1 -0
  74. package/dist/lib/preview-link.js +40 -0
  75. package/dist/lib/preview-link.js.map +1 -0
  76. package/dist/router/index.d.ts +1 -0
  77. package/dist/router/index.d.ts.map +1 -1
  78. package/dist/router/index.js +9 -1
  79. package/dist/router/index.js.map +1 -1
  80. package/dist/views/Dashboard.d.ts.map +1 -1
  81. package/dist/views/Dashboard.js +147 -62
  82. package/dist/views/Dashboard.js.map +1 -1
  83. package/dist/views/Pages/PageTagManager.d.ts +14 -0
  84. package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
  85. package/dist/views/Pages/PageTagManager.js +145 -0
  86. package/dist/views/Pages/PageTagManager.js.map +1 -0
  87. package/dist/views/Pages/PagesListView.d.ts +18 -0
  88. package/dist/views/Pages/PagesListView.d.ts.map +1 -0
  89. package/dist/views/Pages/PagesListView.js +287 -0
  90. package/dist/views/Pages/PagesListView.js.map +1 -0
  91. package/dist/views/Posts/NewPostPicker.d.ts +16 -0
  92. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
  93. package/dist/views/Posts/NewPostPicker.js +59 -0
  94. package/dist/views/Posts/NewPostPicker.js.map +1 -0
  95. package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
  96. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
  97. package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
  98. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
  99. package/dist/views/Posts/PostTypesView.d.ts +26 -0
  100. package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
  101. package/dist/views/Posts/PostTypesView.js +52 -0
  102. package/dist/views/Posts/PostTypesView.js.map +1 -0
  103. package/dist/views/Posts/PostsListView.d.ts +25 -0
  104. package/dist/views/Posts/PostsListView.d.ts.map +1 -0
  105. package/dist/views/Posts/PostsListView.js +239 -0
  106. package/dist/views/Posts/PostsListView.js.map +1 -0
  107. package/dist/views/Posts/post-type-presets.d.ts +32 -0
  108. package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
  109. package/dist/views/Posts/post-type-presets.js +72 -0
  110. package/dist/views/Posts/post-type-presets.js.map +1 -0
  111. package/dist/views/page-builder/Widgets.d.ts +13 -0
  112. package/dist/views/page-builder/Widgets.d.ts.map +1 -0
  113. package/dist/views/page-builder/Widgets.js +15 -0
  114. package/dist/views/page-builder/Widgets.js.map +1 -0
  115. package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
  116. package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
  117. package/dist/views/page-editor/AddSectionModal.js +22 -0
  118. package/dist/views/page-editor/AddSectionModal.js.map +1 -0
  119. package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
  120. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
  121. package/dist/views/page-editor/EditorCanvas.js +64 -0
  122. package/dist/views/page-editor/EditorCanvas.js.map +1 -0
  123. package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
  124. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
  125. package/dist/views/page-editor/EditorTopBar.js +42 -0
  126. package/dist/views/page-editor/EditorTopBar.js.map +1 -0
  127. package/dist/views/page-editor/PagePreview.d.ts +13 -0
  128. package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
  129. package/dist/views/page-editor/PagePreview.js +37 -0
  130. package/dist/views/page-editor/PagePreview.js.map +1 -0
  131. package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
  132. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
  133. package/dist/views/page-editor/PageSectionEditor.js +272 -0
  134. package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
  135. package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
  136. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
  137. package/dist/views/page-editor/PageSettingsModal.js +52 -0
  138. package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
  139. package/dist/views/page-editor/SectionInspector.d.ts +17 -0
  140. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
  141. package/dist/views/page-editor/SectionInspector.js +98 -0
  142. package/dist/views/page-editor/SectionInspector.js.map +1 -0
  143. package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
  144. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
  145. package/dist/views/page-editor/SectionsPanel.js +43 -0
  146. package/dist/views/page-editor/SectionsPanel.js.map +1 -0
  147. package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
  148. package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
  149. package/dist/views/page-editor/ValidationSummary.js +10 -0
  150. package/dist/views/page-editor/ValidationSummary.js.map +1 -0
  151. package/dist/views/page-editor/section-icons.d.ts +4 -0
  152. package/dist/views/page-editor/section-icons.d.ts.map +1 -0
  153. package/dist/views/page-editor/section-icons.js +20 -0
  154. package/dist/views/page-editor/section-icons.js.map +1 -0
  155. package/dist/views/page-editor/section-types.d.ts +11 -0
  156. package/dist/views/page-editor/section-types.d.ts.map +1 -0
  157. package/dist/views/page-editor/section-types.js +10 -0
  158. package/dist/views/page-editor/section-types.js.map +1 -0
  159. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
  160. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
  161. package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
  162. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
  163. package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
  164. package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
  165. package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
  166. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
  167. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
  168. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
  169. package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
  170. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
  171. package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
  172. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
  173. package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
  174. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
  175. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
  176. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
  177. package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
  178. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
  179. package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
  180. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
  181. package/dist/views/page-editor/sections/GallerySection.js +17 -0
  182. package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
  183. package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
  184. package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
  185. package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
  186. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
  187. package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
  188. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
  189. package/dist/views/page-editor/sections/MissionSection.js +12 -0
  190. package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
  191. package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
  192. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
  193. package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
  194. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
  195. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
  196. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
  197. package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
  198. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
  199. package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
  200. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
  201. package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
  202. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
  203. package/dist/views/page-editor/sections/index.d.ts +14 -0
  204. package/dist/views/page-editor/sections/index.d.ts.map +1 -0
  205. package/dist/views/page-editor/sections/index.js +12 -0
  206. package/dist/views/page-editor/sections/index.js.map +1 -0
  207. package/dist/views/page-editor/sections/parts.d.ts +55 -0
  208. package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
  209. package/dist/views/page-editor/sections/parts.js +60 -0
  210. package/dist/views/page-editor/sections/parts.js.map +1 -0
  211. package/dist/views/page-editor/viewports.d.ts +14 -0
  212. package/dist/views/page-editor/viewports.d.ts.map +1 -0
  213. package/dist/views/page-editor/viewports.js +12 -0
  214. package/dist/views/page-editor/viewports.js.map +1 -0
  215. package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
  216. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
  217. package/dist/views/post-editor/PostEditorCanvas.js +58 -0
  218. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
  219. package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
  220. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
  221. package/dist/views/post-editor/PostFieldsModal.js +53 -0
  222. package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
  223. package/dist/views/post-editor/PostHeader.d.ts +15 -0
  224. package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
  225. package/dist/views/post-editor/PostHeader.js +27 -0
  226. package/dist/views/post-editor/PostHeader.js.map +1 -0
  227. package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
  228. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
  229. package/dist/views/post-editor/PostHeaderPanel.js +21 -0
  230. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
  231. package/dist/views/post-editor/PostPreview.d.ts +14 -0
  232. package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
  233. package/dist/views/post-editor/PostPreview.js +53 -0
  234. package/dist/views/post-editor/PostPreview.js.map +1 -0
  235. package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
  236. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
  237. package/dist/views/post-editor/PostSectionEditor.js +290 -0
  238. package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
  239. package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
  240. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
  241. package/dist/views/post-editor/PostTemplateEditor.js +144 -0
  242. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
  243. package/package.json +2 -2
  244. package/src/AdminRoot.tsx +213 -2
  245. package/src/__tests__/components/posts-badges.test.tsx +77 -0
  246. package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
  247. package/src/__tests__/lib/page-editor-service.test.ts +173 -0
  248. package/src/__tests__/lib/pages-service.test.ts +157 -0
  249. package/src/__tests__/lib/post-editor-service.test.ts +215 -0
  250. package/src/__tests__/lib/posts-service.test.ts +279 -0
  251. package/src/__tests__/views/posts-list-view.test.ts +110 -0
  252. package/src/components/pages/tag-badges.tsx +104 -0
  253. package/src/components/posts/badges.tsx +170 -0
  254. package/src/components/posts/icons.ts +40 -0
  255. package/src/layout/Header.tsx +90 -43
  256. package/src/layout/Layout.tsx +166 -1
  257. package/src/layout/Sidebar.tsx +287 -101
  258. package/src/lib/page-editor-service.ts +233 -0
  259. package/src/lib/pages-service.ts +599 -0
  260. package/src/lib/post-editor-service.ts +342 -0
  261. package/src/lib/posts-service.ts +366 -0
  262. package/src/lib/preview-link.ts +63 -0
  263. package/src/router/index.ts +10 -1
  264. package/src/views/Dashboard.tsx +270 -131
  265. package/src/views/Pages/PageTagManager.tsx +436 -0
  266. package/src/views/Pages/PagesListView.tsx +944 -0
  267. package/src/views/Posts/NewPostPicker.tsx +147 -0
  268. package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
  269. package/src/views/Posts/PostTypesView.tsx +194 -0
  270. package/src/views/Posts/PostsListView.tsx +869 -0
  271. package/src/views/Posts/post-type-presets.ts +105 -0
  272. package/src/views/page-builder/Widgets.tsx +44 -0
  273. package/src/views/page-editor/AddSectionModal.tsx +56 -0
  274. package/src/views/page-editor/EditorCanvas.tsx +127 -0
  275. package/src/views/page-editor/EditorTopBar.tsx +233 -0
  276. package/src/views/page-editor/PagePreview.tsx +96 -0
  277. package/src/views/page-editor/PageSectionEditor.tsx +476 -0
  278. package/src/views/page-editor/PageSettingsModal.tsx +154 -0
  279. package/src/views/page-editor/SectionInspector.tsx +599 -0
  280. package/src/views/page-editor/SectionsPanel.tsx +270 -0
  281. package/src/views/page-editor/ValidationSummary.tsx +53 -0
  282. package/src/views/page-editor/section-icons.ts +33 -0
  283. package/src/views/page-editor/section-types.ts +27 -0
  284. package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
  285. package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
  286. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
  287. package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
  288. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
  289. package/src/views/page-editor/sections/GallerySection.tsx +54 -0
  290. package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
  291. package/src/views/page-editor/sections/MissionSection.tsx +22 -0
  292. package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
  293. package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
  294. package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
  295. package/src/views/page-editor/sections/index.ts +13 -0
  296. package/src/views/page-editor/sections/parts.tsx +113 -0
  297. package/src/views/page-editor/viewports.ts +25 -0
  298. package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
  299. package/src/views/post-editor/PostFieldsModal.tsx +196 -0
  300. package/src/views/post-editor/PostHeader.tsx +87 -0
  301. package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
  302. package/src/views/post-editor/PostPreview.tsx +114 -0
  303. package/src/views/post-editor/PostSectionEditor.tsx +506 -0
  304. package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
@@ -0,0 +1,44 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { PreviewButton, isDarkBackground, str } from './parts.js'
3
+
4
+ export function HeroBannerSection({ section }: { section: PageSection }) {
5
+ const c = section.content
6
+ const dark = isDarkBackground(section.settings)
7
+ const eyebrow = str(c, 'eyebrow')
8
+ const heading = str(c, 'heading')
9
+ const body = str(c, 'body')
10
+
11
+ return (
12
+ <div className="mx-auto max-w-3xl px-6 text-center">
13
+ {eyebrow && (
14
+ <p
15
+ className={`mb-3 text-sm font-medium tracking-wide uppercase ${dark ? 'text-violet-200' : 'text-violet-600'}`}
16
+ >
17
+ {eyebrow}
18
+ </p>
19
+ )}
20
+ <h1 className="text-3xl leading-tight font-semibold tracking-tight sm:text-5xl">
21
+ {heading || 'Your headline goes here'}
22
+ </h1>
23
+ {body && (
24
+ <p
25
+ className={`mx-auto mt-5 max-w-2xl text-base sm:text-lg ${dark ? 'text-violet-100' : 'text-zinc-600'}`}
26
+ >
27
+ {body}
28
+ </p>
29
+ )}
30
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
31
+ <PreviewButton
32
+ label={str(c, 'primaryButtonLabel')}
33
+ href={str(c, 'primaryButtonUrl')}
34
+ tone={dark ? 'onDark' : 'primary'}
35
+ />
36
+ <PreviewButton
37
+ label={str(c, 'secondaryButtonLabel')}
38
+ href={str(c, 'secondaryButtonUrl')}
39
+ tone="ghost"
40
+ />
41
+ </div>
42
+ </div>
43
+ )
44
+ }
@@ -0,0 +1,22 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { str } from './parts.js'
3
+
4
+ /** Generic "Text Block" / mission statement section. */
5
+ export function MissionSection({ section }: { section: PageSection }) {
6
+ const c = section.content
7
+ const alignment = str(c, 'alignment', 'center')
8
+ const align = alignment === 'left' ? 'text-left' : 'text-center mx-auto'
9
+ const heading = str(c, 'heading')
10
+ const body = str(c, 'body')
11
+
12
+ return (
13
+ <div className={`max-w-3xl px-6 ${align}`}>
14
+ {heading && (
15
+ <h2 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
16
+ {heading}
17
+ </h2>
18
+ )}
19
+ {body && <p className="mt-4 text-base leading-relaxed text-zinc-600 sm:text-lg">{body}</p>}
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { str } from './parts.js'
3
+
4
+ /** A highlighted pull quote with optional attribution. */
5
+ export function PullQuoteSection({ section }: { section: PageSection }) {
6
+ const c = section.content
7
+ const quote = str(c, 'quote')
8
+ const attribution = str(c, 'attribution')
9
+ const role = str(c, 'role')
10
+
11
+ if (!quote) return null
12
+
13
+ return (
14
+ <figure className="mx-auto max-w-3xl px-6">
15
+ <blockquote className="border-l-4 border-violet-500 pl-6">
16
+ <p className="text-xl leading-relaxed font-medium text-zinc-800 sm:text-2xl">“{quote}”</p>
17
+ </blockquote>
18
+ {(attribution || role) && (
19
+ <figcaption className="mt-3 pl-6 text-sm text-zinc-500">
20
+ {attribution && <span className="font-medium text-zinc-700">{attribution}</span>}
21
+ {attribution && role && <span aria-hidden> · </span>}
22
+ {role}
23
+ </figcaption>
24
+ )}
25
+ </figure>
26
+ )
27
+ }
@@ -0,0 +1,55 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { str, type PostRenderContext } from './parts.js'
3
+
4
+ /**
5
+ * A grid of related posts. Uses resolved posts from context when available;
6
+ * otherwise renders placeholder cards so the layout reads correctly in the
7
+ * editor before data is wired up.
8
+ */
9
+ export function RelatedPostsSection({
10
+ section,
11
+ context,
12
+ }: {
13
+ section: PageSection
14
+ context?: PostRenderContext
15
+ }) {
16
+ const c = section.content
17
+ const heading = str(c, 'heading', 'Related posts')
18
+ const count = Number.parseInt(str(c, 'count', '3'), 10) || 3
19
+
20
+ const resolved = context?.relatedPosts ?? []
21
+ const cards =
22
+ resolved.length > 0
23
+ ? resolved.slice(0, count)
24
+ : Array.from({ length: count }, (_, i) => ({
25
+ title: `Related post ${i + 1}`,
26
+ excerpt: 'A short summary of the related post will appear here.',
27
+ url: undefined as string | undefined,
28
+ }))
29
+
30
+ const cols = count >= 4 ? 'sm:grid-cols-2 lg:grid-cols-4' : 'sm:grid-cols-3'
31
+
32
+ return (
33
+ <div className="mx-auto max-w-5xl px-6">
34
+ {heading && (
35
+ <h2 className="mb-6 text-2xl font-semibold tracking-tight text-zinc-900">{heading}</h2>
36
+ )}
37
+ <div className={`grid grid-cols-1 gap-4 ${cols}`}>
38
+ {cards.map((post, i) => (
39
+ <div
40
+ key={i}
41
+ className="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm"
42
+ >
43
+ <div className="aspect-[16/9] bg-zinc-100" aria-hidden />
44
+ <div className="p-4">
45
+ <p className="text-base font-medium text-zinc-900">{post.title}</p>
46
+ {post.excerpt && (
47
+ <p className="mt-1 line-clamp-2 text-sm text-zinc-500">{post.excerpt}</p>
48
+ )}
49
+ </div>
50
+ </div>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,102 @@
1
+ 'use client'
2
+
3
+ import type { PageSection } from '../../../lib/page-editor-service.js'
4
+ import { backgroundClass, paddingClass, type PostRenderContext } from './parts.js'
5
+ import { HeroBannerSection } from './HeroBannerSection.js'
6
+ import { MissionSection } from './MissionSection.js'
7
+ import { FeatureHighlightSection } from './FeatureHighlightSection.js'
8
+ import { ByTheNumbersSection } from './ByTheNumbersSection.js'
9
+ import { CtaBannerSection } from './CtaBannerSection.js'
10
+ import { ArticleBodySection } from './ArticleBodySection.js'
11
+ import { PullQuoteSection } from './PullQuoteSection.js'
12
+ import { AuthorBioSection } from './AuthorBioSection.js'
13
+ import { RelatedPostsSection } from './RelatedPostsSection.js'
14
+ import { GallerySection } from './GallerySection.js'
15
+
16
+ /** Dispatch a section's body component by type. Shared by editor + preview. */
17
+ function SectionBody({ section, context }: { section: PageSection; context?: PostRenderContext }) {
18
+ switch (section.sectionType) {
19
+ case 'hero':
20
+ return <HeroBannerSection section={section} />
21
+ case 'text':
22
+ return <MissionSection section={section} />
23
+ case 'feature':
24
+ return <FeatureHighlightSection section={section} />
25
+ case 'stats':
26
+ return <ByTheNumbersSection section={section} />
27
+ case 'cta':
28
+ return <CtaBannerSection section={section} />
29
+ case 'article-body':
30
+ return <ArticleBodySection section={section} context={context} />
31
+ case 'quote':
32
+ return <PullQuoteSection section={section} />
33
+ case 'author-bio':
34
+ return <AuthorBioSection section={section} context={context} />
35
+ case 'related-posts':
36
+ return <RelatedPostsSection section={section} context={context} />
37
+ case 'gallery':
38
+ return <GallerySection section={section} />
39
+ default:
40
+ return null
41
+ }
42
+ }
43
+
44
+ export interface SectionRendererProps {
45
+ section: PageSection
46
+ /** Editor mode: adds click-to-select, the selection outline + label, and
47
+ * renders hidden sections muted instead of removing them. */
48
+ editable?: boolean
49
+ selected?: boolean
50
+ onSelect?: (id: string) => void
51
+ /** Lets the canvas register the DOM node for scroll-to-section. */
52
+ registerRef?: (id: string, el: HTMLElement | null) => void
53
+ /** Live post data for post-oriented section renderers (pages omit this). */
54
+ context?: PostRenderContext
55
+ }
56
+
57
+ /**
58
+ * Renders one section with its background + padding, dispatching to the
59
+ * shared body component. In `editable` mode it layers editor-only chrome
60
+ * (outline + a small purple type label) on top of the same markup the
61
+ * public preview uses — so what you edit is what ships.
62
+ */
63
+ export function SectionRenderer({
64
+ section,
65
+ editable = false,
66
+ selected = false,
67
+ onSelect,
68
+ registerRef,
69
+ context,
70
+ }: SectionRendererProps) {
71
+ const settings = section.settings ?? {}
72
+ const hidden = editable && !section.visible
73
+
74
+ return (
75
+ <section
76
+ ref={(el) => registerRef?.(section.id, el)}
77
+ data-section-id={section.id}
78
+ id={settings.anchorId || undefined}
79
+ className={`relative ${backgroundClass(settings)} ${paddingClass(settings)} ${
80
+ settings.cssClass ?? ''
81
+ } ${hidden ? 'opacity-40' : ''} ${
82
+ editable ? 'cursor-pointer outline-2 -outline-offset-2 transition-[outline]' : ''
83
+ } ${selected ? 'outline outline-violet-500' : 'outline-transparent'}`}
84
+ onClick={editable ? () => onSelect?.(section.id) : undefined}
85
+ // Selection is a convenience in the editor; the section list is the
86
+ // primary, fully-accessible control surface, so this stays a region.
87
+ aria-label={editable ? `${section.name} section` : undefined}
88
+ >
89
+ {editable && selected && (
90
+ <span className="pointer-events-none absolute top-0 left-0 z-10 rounded-br-md bg-violet-600 px-2 py-0.5 text-xs font-medium text-white">
91
+ {section.name}
92
+ </span>
93
+ )}
94
+ {editable && hidden && (
95
+ <span className="pointer-events-none absolute top-0 right-0 z-10 rounded-bl-md bg-zinc-700 px-2 py-0.5 text-xs font-medium text-white">
96
+ Hidden
97
+ </span>
98
+ )}
99
+ <SectionBody section={section} context={context} />
100
+ </section>
101
+ )
102
+ }
@@ -0,0 +1,13 @@
1
+ export { SectionRenderer } from './SectionRenderer.js'
2
+ export type { SectionRendererProps } from './SectionRenderer.js'
3
+ export type { PostRenderContext } from './parts.js'
4
+ export { HeroBannerSection } from './HeroBannerSection.js'
5
+ export { MissionSection } from './MissionSection.js'
6
+ export { FeatureHighlightSection } from './FeatureHighlightSection.js'
7
+ export { ByTheNumbersSection } from './ByTheNumbersSection.js'
8
+ export { CtaBannerSection } from './CtaBannerSection.js'
9
+ export { ArticleBodySection } from './ArticleBodySection.js'
10
+ export { PullQuoteSection } from './PullQuoteSection.js'
11
+ export { AuthorBioSection } from './AuthorBioSection.js'
12
+ export { RelatedPostsSection } from './RelatedPostsSection.js'
13
+ export { GallerySection } from './GallerySection.js'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared building blocks for the section renderers.
3
+ *
4
+ * NOTE ON STYLING: the components under `sections/` render the consumer's
5
+ * *public website* content (purple hero, white prose bands, etc.), not the
6
+ * admin chrome. They therefore use a small, fixed website palette (violet
7
+ * for primary, zinc for neutrals) instead of the admin theme tokens. This
8
+ * is deliberate and scoped to this folder so the preview looks like a real
9
+ * page; the surrounding editor UI still uses the admin design tokens.
10
+ */
11
+ import type { SectionSettings } from '../section-types.js'
12
+
13
+ /**
14
+ * Live post data passed to the post-oriented section renderers
15
+ * (article body, author bio, related posts). Pages never supply this, so
16
+ * every consumer treats it as optional and falls back to placeholder copy.
17
+ */
18
+ export interface PostRenderContext {
19
+ title?: string
20
+ excerpt?: string
21
+ /** the post's main body copy (rendered by the Article Body section) */
22
+ body?: string
23
+ featuredImageUrl?: string
24
+ publishDate?: string | null
25
+ category?: string
26
+ author?: { name?: string; bio?: string; avatarUrl?: string }
27
+ /** resolved related posts for the Related Posts section */
28
+ relatedPosts?: Array<{ title: string; url?: string; excerpt?: string }>
29
+ }
30
+
31
+ export function str(content: Record<string, unknown>, key: string, fallback = ''): string {
32
+ const v = content[key]
33
+ return typeof v === 'string' ? v : fallback
34
+ }
35
+
36
+ export function bool(content: Record<string, unknown>, key: string, fallback = false): boolean {
37
+ const v = content[key]
38
+ return typeof v === 'boolean' ? v : fallback
39
+ }
40
+
41
+ /** Vertical padding scale for a section. */
42
+ export function paddingClass(settings: SectionSettings): string {
43
+ switch (settings.paddingY) {
44
+ case 'sm':
45
+ return 'py-8'
46
+ case 'md':
47
+ return 'py-12'
48
+ case 'xl':
49
+ return 'py-20 sm:py-28'
50
+ case 'lg':
51
+ default:
52
+ return 'py-16 sm:py-20'
53
+ }
54
+ }
55
+
56
+ /** Returns true when the section background is the dark/purple treatment. */
57
+ export function isDarkBackground(settings: SectionSettings): boolean {
58
+ return (
59
+ (settings.backgroundType === 'solid' || settings.backgroundType === 'gradient') &&
60
+ settings.backgroundColor === 'purple'
61
+ )
62
+ }
63
+
64
+ /** Background utility classes derived from settings. */
65
+ export function backgroundClass(settings: SectionSettings): string {
66
+ if (settings.backgroundType === 'gradient' && settings.backgroundColor === 'purple') {
67
+ return 'bg-gradient-to-br from-violet-600 to-violet-700 text-white'
68
+ }
69
+ if (settings.backgroundType === 'solid' && settings.backgroundColor === 'purple') {
70
+ return 'bg-violet-600 text-white'
71
+ }
72
+ if (settings.backgroundType === 'solid' && settings.backgroundColor === 'muted') {
73
+ return 'bg-zinc-50 text-zinc-900'
74
+ }
75
+ return 'bg-white text-zinc-900'
76
+ }
77
+
78
+ /**
79
+ * Preview-only button. `tone="onDark"` renders the white-on-purple button
80
+ * from the screenshot; `tone="primary"` renders a solid violet button for
81
+ * light backgrounds.
82
+ */
83
+ export function PreviewButton({
84
+ label,
85
+ href,
86
+ tone = 'primary',
87
+ }: {
88
+ label: string
89
+ href?: string
90
+ tone?: 'primary' | 'onDark' | 'ghost'
91
+ }) {
92
+ if (!label) return null
93
+ const base =
94
+ 'inline-flex items-center justify-center rounded-lg px-5 py-2.5 text-sm font-medium transition-colors'
95
+ const toneClass =
96
+ tone === 'onDark'
97
+ ? 'bg-white text-violet-700 hover:bg-violet-50'
98
+ : tone === 'ghost'
99
+ ? 'border border-zinc-300 text-zinc-700 hover:bg-zinc-50'
100
+ : 'bg-violet-600 text-white hover:bg-violet-700'
101
+ // Editor preview: links are inert (preventDefault) so clicking content
102
+ // selects the section instead of navigating away.
103
+ return (
104
+ <a
105
+ href={href || '#'}
106
+ onClick={(e) => e.preventDefault()}
107
+ className={`${base} ${toneClass}`}
108
+ tabIndex={-1}
109
+ >
110
+ {label}
111
+ </a>
112
+ )
113
+ }
@@ -0,0 +1,25 @@
1
+ /** Preview viewport presets for the page editor canvas. */
2
+ export type ViewportId = '4k' | '1080p' | 'laptop' | 'tablet' | 'mobile'
3
+
4
+ export interface Viewport {
5
+ id: ViewportId
6
+ label: string
7
+ /** lucide-react icon name */
8
+ icon: string
9
+ /** logical CSS width of the preview surface, in px */
10
+ width: number
11
+ }
12
+
13
+ export const VIEWPORTS: Viewport[] = [
14
+ { id: '4k', label: '4K', icon: 'monitor', width: 1920 },
15
+ { id: '1080p', label: '1080p', icon: 'monitor', width: 1440 },
16
+ { id: 'laptop', label: 'Laptop', icon: 'laptop', width: 1280 },
17
+ { id: 'tablet', label: 'Tablet', icon: 'tablet', width: 768 },
18
+ { id: 'mobile', label: 'Mobile', icon: 'smartphone', width: 390 },
19
+ ]
20
+
21
+ export const DEFAULT_VIEWPORT: ViewportId = '1080p'
22
+
23
+ export function getViewport(id: ViewportId): Viewport {
24
+ return VIEWPORTS.find((v) => v.id === id) ?? VIEWPORTS[1]!
25
+ }
@@ -0,0 +1,123 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
4
+ import { FileText, Plus } from 'lucide-react'
5
+ import type { PageSection, PostHeaderConfig } from '../../lib/post-editor-service.js'
6
+ import { SectionRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
7
+ import { getViewport, type ViewportId } from '../page-editor/viewports.js'
8
+ import { PostHeader } from './PostHeader.js'
9
+
10
+ interface PostEditorCanvasProps {
11
+ sections: PageSection[]
12
+ header: PostHeaderConfig
13
+ context: PostRenderContext
14
+ selectedId: string | null
15
+ viewport: ViewportId
16
+ onSelect: (id: string) => void
17
+ onAddSection: () => void
18
+ }
19
+
20
+ /**
21
+ * Post preview canvas. Mirrors the Page editor canvas (scaled surface that
22
+ * fits the selected viewport) but renders the post header above the section
23
+ * list so the editor shows the full post layout — header regions plus body
24
+ * sections — exactly as it will ship.
25
+ */
26
+ export function PostEditorCanvas({
27
+ sections,
28
+ header,
29
+ context,
30
+ selectedId,
31
+ viewport,
32
+ onSelect,
33
+ onAddSection,
34
+ }: PostEditorCanvasProps) {
35
+ const vp = getViewport(viewport)
36
+ const containerRef = useRef<HTMLDivElement>(null)
37
+ const surfaceRef = useRef<HTMLDivElement>(null)
38
+ const sectionRefs = useRef<Map<string, HTMLElement>>(new Map())
39
+ const [scale, setScale] = useState(1)
40
+ const [surfaceHeight, setSurfaceHeight] = useState<number | null>(null)
41
+
42
+ const registerRef = useCallback((id: string, el: HTMLElement | null) => {
43
+ if (el) sectionRefs.current.set(id, el)
44
+ else sectionRefs.current.delete(id)
45
+ }, [])
46
+
47
+ useLayoutEffect(() => {
48
+ const container = containerRef.current
49
+ if (!container) return
50
+ const recompute = () => {
51
+ const available = container.clientWidth - 48
52
+ const next = Math.min(1, available / vp.width)
53
+ setScale(next > 0 ? next : 1)
54
+ if (surfaceRef.current) {
55
+ setSurfaceHeight(surfaceRef.current.offsetHeight * (next > 0 ? next : 1))
56
+ }
57
+ }
58
+ recompute()
59
+ const ro = new ResizeObserver(recompute)
60
+ ro.observe(container)
61
+ if (surfaceRef.current) ro.observe(surfaceRef.current)
62
+ return () => ro.disconnect()
63
+ }, [vp.width, sections, header, context])
64
+
65
+ useEffect(() => {
66
+ if (!selectedId) return
67
+ const el = sectionRefs.current.get(selectedId)
68
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
69
+ }, [selectedId])
70
+
71
+ return (
72
+ <div ref={containerRef} className="bg-muted h-full overflow-auto p-6">
73
+ <div
74
+ className="mx-auto"
75
+ style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
76
+ >
77
+ <div
78
+ ref={surfaceRef}
79
+ className="border-border origin-top overflow-hidden rounded-xl border bg-white shadow-sm"
80
+ style={{ width: vp.width, transform: `scale(${scale})`, transformOrigin: 'top left' }}
81
+ >
82
+ <PostHeader config={header} context={context} />
83
+ {sections.length === 0 ? (
84
+ <EmptyBody onAddSection={onAddSection} />
85
+ ) : (
86
+ sections.map((section) => (
87
+ <SectionRenderer
88
+ key={section.id}
89
+ section={section}
90
+ context={context}
91
+ editable
92
+ selected={section.id === selectedId}
93
+ onSelect={onSelect}
94
+ registerRef={registerRef}
95
+ />
96
+ ))
97
+ )}
98
+ </div>
99
+ </div>
100
+ </div>
101
+ )
102
+ }
103
+
104
+ function EmptyBody({ onAddSection }: { onAddSection: () => void }) {
105
+ return (
106
+ <div className="flex flex-col items-center justify-center gap-3 border-t border-zinc-100 px-6 py-16 text-center">
107
+ <span className="flex h-12 w-12 items-center justify-center rounded-full bg-zinc-100">
108
+ <FileText className="h-5 w-5 text-zinc-400" aria-hidden />
109
+ </span>
110
+ <p className="text-sm text-zinc-500">
111
+ The header is set by the template. Add body sections to build out this post.
112
+ </p>
113
+ <button
114
+ type="button"
115
+ onClick={onAddSection}
116
+ className="inline-flex items-center gap-2 rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-violet-700"
117
+ >
118
+ <Plus className="h-4 w-4" aria-hidden />
119
+ Add Section
120
+ </button>
121
+ </div>
122
+ )
123
+ }