@actuate-media/cms-admin 0.15.0 → 0.16.1

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 (308) 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__/lib/useApiData.test.d.ts +2 -0
  29. package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
  30. package/dist/__tests__/lib/useApiData.test.js +84 -0
  31. package/dist/__tests__/lib/useApiData.test.js.map +1 -0
  32. package/dist/__tests__/views/dashboard.test.d.ts +2 -0
  33. package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
  34. package/dist/__tests__/views/dashboard.test.js +138 -0
  35. package/dist/__tests__/views/dashboard.test.js.map +1 -0
  36. package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
  37. package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
  38. package/dist/__tests__/views/posts-list-view.test.js +98 -0
  39. package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
  40. package/dist/actuate-admin.css +1 -1
  41. package/dist/components/pages/tag-badges.d.ts +21 -0
  42. package/dist/components/pages/tag-badges.d.ts.map +1 -0
  43. package/dist/components/pages/tag-badges.js +56 -0
  44. package/dist/components/pages/tag-badges.js.map +1 -0
  45. package/dist/components/posts/badges.d.ts +25 -0
  46. package/dist/components/posts/badges.d.ts.map +1 -0
  47. package/dist/components/posts/badges.js +123 -0
  48. package/dist/components/posts/badges.js.map +1 -0
  49. package/dist/components/posts/icons.d.ts +12 -0
  50. package/dist/components/posts/icons.d.ts.map +1 -0
  51. package/dist/components/posts/icons.js +26 -0
  52. package/dist/components/posts/icons.js.map +1 -0
  53. package/dist/layout/Layout.js +36 -8
  54. package/dist/layout/Layout.js.map +1 -1
  55. package/dist/layout/Sidebar.d.ts +9 -0
  56. package/dist/layout/Sidebar.d.ts.map +1 -1
  57. package/dist/layout/Sidebar.js +100 -48
  58. package/dist/layout/Sidebar.js.map +1 -1
  59. package/dist/lib/page-editor-service.d.ts +45 -0
  60. package/dist/lib/page-editor-service.d.ts.map +1 -0
  61. package/dist/lib/page-editor-service.js +153 -0
  62. package/dist/lib/page-editor-service.js.map +1 -0
  63. package/dist/lib/pages-service.d.ts +143 -0
  64. package/dist/lib/pages-service.d.ts.map +1 -0
  65. package/dist/lib/pages-service.js +444 -0
  66. package/dist/lib/pages-service.js.map +1 -0
  67. package/dist/lib/post-editor-service.d.ts +57 -0
  68. package/dist/lib/post-editor-service.d.ts.map +1 -0
  69. package/dist/lib/post-editor-service.js +234 -0
  70. package/dist/lib/post-editor-service.js.map +1 -0
  71. package/dist/lib/posts-service.d.ts +73 -0
  72. package/dist/lib/posts-service.d.ts.map +1 -0
  73. package/dist/lib/posts-service.js +245 -0
  74. package/dist/lib/posts-service.js.map +1 -0
  75. package/dist/lib/preview-link.d.ts +32 -0
  76. package/dist/lib/preview-link.d.ts.map +1 -0
  77. package/dist/lib/preview-link.js +40 -0
  78. package/dist/lib/preview-link.js.map +1 -0
  79. package/dist/router/index.d.ts +1 -0
  80. package/dist/router/index.d.ts.map +1 -1
  81. package/dist/router/index.js +9 -1
  82. package/dist/router/index.js.map +1 -1
  83. package/dist/views/Dashboard.d.ts.map +1 -1
  84. package/dist/views/Dashboard.js +64 -20
  85. package/dist/views/Dashboard.js.map +1 -1
  86. package/dist/views/Pages/PageTagManager.d.ts +14 -0
  87. package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
  88. package/dist/views/Pages/PageTagManager.js +145 -0
  89. package/dist/views/Pages/PageTagManager.js.map +1 -0
  90. package/dist/views/Pages/PagesListView.d.ts +18 -0
  91. package/dist/views/Pages/PagesListView.d.ts.map +1 -0
  92. package/dist/views/Pages/PagesListView.js +287 -0
  93. package/dist/views/Pages/PagesListView.js.map +1 -0
  94. package/dist/views/Posts/NewPostPicker.d.ts +16 -0
  95. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
  96. package/dist/views/Posts/NewPostPicker.js +59 -0
  97. package/dist/views/Posts/NewPostPicker.js.map +1 -0
  98. package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
  99. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
  100. package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
  101. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
  102. package/dist/views/Posts/PostTypesView.d.ts +26 -0
  103. package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
  104. package/dist/views/Posts/PostTypesView.js +52 -0
  105. package/dist/views/Posts/PostTypesView.js.map +1 -0
  106. package/dist/views/Posts/PostsListView.d.ts +25 -0
  107. package/dist/views/Posts/PostsListView.d.ts.map +1 -0
  108. package/dist/views/Posts/PostsListView.js +239 -0
  109. package/dist/views/Posts/PostsListView.js.map +1 -0
  110. package/dist/views/Posts/post-type-presets.d.ts +32 -0
  111. package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
  112. package/dist/views/Posts/post-type-presets.js +72 -0
  113. package/dist/views/Posts/post-type-presets.js.map +1 -0
  114. package/dist/views/page-builder/Widgets.d.ts +13 -0
  115. package/dist/views/page-builder/Widgets.d.ts.map +1 -0
  116. package/dist/views/page-builder/Widgets.js +15 -0
  117. package/dist/views/page-builder/Widgets.js.map +1 -0
  118. package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
  119. package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
  120. package/dist/views/page-editor/AddSectionModal.js +22 -0
  121. package/dist/views/page-editor/AddSectionModal.js.map +1 -0
  122. package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
  123. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
  124. package/dist/views/page-editor/EditorCanvas.js +64 -0
  125. package/dist/views/page-editor/EditorCanvas.js.map +1 -0
  126. package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
  127. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
  128. package/dist/views/page-editor/EditorTopBar.js +42 -0
  129. package/dist/views/page-editor/EditorTopBar.js.map +1 -0
  130. package/dist/views/page-editor/PagePreview.d.ts +13 -0
  131. package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
  132. package/dist/views/page-editor/PagePreview.js +37 -0
  133. package/dist/views/page-editor/PagePreview.js.map +1 -0
  134. package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
  135. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
  136. package/dist/views/page-editor/PageSectionEditor.js +272 -0
  137. package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
  138. package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
  139. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
  140. package/dist/views/page-editor/PageSettingsModal.js +52 -0
  141. package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
  142. package/dist/views/page-editor/SectionInspector.d.ts +17 -0
  143. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
  144. package/dist/views/page-editor/SectionInspector.js +98 -0
  145. package/dist/views/page-editor/SectionInspector.js.map +1 -0
  146. package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
  147. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
  148. package/dist/views/page-editor/SectionsPanel.js +43 -0
  149. package/dist/views/page-editor/SectionsPanel.js.map +1 -0
  150. package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
  151. package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
  152. package/dist/views/page-editor/ValidationSummary.js +10 -0
  153. package/dist/views/page-editor/ValidationSummary.js.map +1 -0
  154. package/dist/views/page-editor/section-icons.d.ts +4 -0
  155. package/dist/views/page-editor/section-icons.d.ts.map +1 -0
  156. package/dist/views/page-editor/section-icons.js +20 -0
  157. package/dist/views/page-editor/section-icons.js.map +1 -0
  158. package/dist/views/page-editor/section-types.d.ts +11 -0
  159. package/dist/views/page-editor/section-types.d.ts.map +1 -0
  160. package/dist/views/page-editor/section-types.js +10 -0
  161. package/dist/views/page-editor/section-types.js.map +1 -0
  162. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
  163. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
  164. package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
  165. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
  166. package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
  167. package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
  168. package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
  169. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
  170. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
  171. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
  172. package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
  173. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
  174. package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
  175. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
  176. package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
  177. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
  178. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
  179. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
  180. package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
  181. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
  182. package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
  183. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
  184. package/dist/views/page-editor/sections/GallerySection.js +17 -0
  185. package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
  186. package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
  187. package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
  188. package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
  189. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
  190. package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
  191. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
  192. package/dist/views/page-editor/sections/MissionSection.js +12 -0
  193. package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
  194. package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
  195. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
  196. package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
  197. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
  198. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
  199. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
  200. package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
  201. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
  202. package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
  203. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
  204. package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
  205. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
  206. package/dist/views/page-editor/sections/index.d.ts +14 -0
  207. package/dist/views/page-editor/sections/index.d.ts.map +1 -0
  208. package/dist/views/page-editor/sections/index.js +12 -0
  209. package/dist/views/page-editor/sections/index.js.map +1 -0
  210. package/dist/views/page-editor/sections/parts.d.ts +55 -0
  211. package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
  212. package/dist/views/page-editor/sections/parts.js +60 -0
  213. package/dist/views/page-editor/sections/parts.js.map +1 -0
  214. package/dist/views/page-editor/viewports.d.ts +14 -0
  215. package/dist/views/page-editor/viewports.d.ts.map +1 -0
  216. package/dist/views/page-editor/viewports.js +12 -0
  217. package/dist/views/page-editor/viewports.js.map +1 -0
  218. package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
  219. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
  220. package/dist/views/post-editor/PostEditorCanvas.js +58 -0
  221. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
  222. package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
  223. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
  224. package/dist/views/post-editor/PostFieldsModal.js +53 -0
  225. package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
  226. package/dist/views/post-editor/PostHeader.d.ts +15 -0
  227. package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
  228. package/dist/views/post-editor/PostHeader.js +27 -0
  229. package/dist/views/post-editor/PostHeader.js.map +1 -0
  230. package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
  231. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
  232. package/dist/views/post-editor/PostHeaderPanel.js +21 -0
  233. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
  234. package/dist/views/post-editor/PostPreview.d.ts +14 -0
  235. package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
  236. package/dist/views/post-editor/PostPreview.js +53 -0
  237. package/dist/views/post-editor/PostPreview.js.map +1 -0
  238. package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
  239. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
  240. package/dist/views/post-editor/PostSectionEditor.js +290 -0
  241. package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
  242. package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
  243. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
  244. package/dist/views/post-editor/PostTemplateEditor.js +144 -0
  245. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
  246. package/package.json +2 -2
  247. package/src/AdminRoot.tsx +214 -2
  248. package/src/__tests__/components/posts-badges.test.tsx +77 -0
  249. package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
  250. package/src/__tests__/lib/page-editor-service.test.ts +173 -0
  251. package/src/__tests__/lib/pages-service.test.ts +157 -0
  252. package/src/__tests__/lib/post-editor-service.test.ts +215 -0
  253. package/src/__tests__/lib/posts-service.test.ts +279 -0
  254. package/src/__tests__/lib/useApiData.test.ts +105 -0
  255. package/src/__tests__/views/dashboard.test.tsx +180 -0
  256. package/src/__tests__/views/posts-list-view.test.ts +110 -0
  257. package/src/components/pages/tag-badges.tsx +104 -0
  258. package/src/components/posts/badges.tsx +170 -0
  259. package/src/components/posts/icons.ts +40 -0
  260. package/src/layout/Layout.tsx +66 -10
  261. package/src/layout/Sidebar.tsx +116 -50
  262. package/src/lib/page-editor-service.ts +233 -0
  263. package/src/lib/pages-service.ts +599 -0
  264. package/src/lib/post-editor-service.ts +342 -0
  265. package/src/lib/posts-service.ts +366 -0
  266. package/src/lib/preview-link.ts +63 -0
  267. package/src/router/index.ts +10 -1
  268. package/src/views/Dashboard.tsx +142 -89
  269. package/src/views/Pages/PageTagManager.tsx +436 -0
  270. package/src/views/Pages/PagesListView.tsx +944 -0
  271. package/src/views/Posts/NewPostPicker.tsx +147 -0
  272. package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
  273. package/src/views/Posts/PostTypesView.tsx +194 -0
  274. package/src/views/Posts/PostsListView.tsx +869 -0
  275. package/src/views/Posts/post-type-presets.ts +105 -0
  276. package/src/views/page-builder/Widgets.tsx +44 -0
  277. package/src/views/page-editor/AddSectionModal.tsx +56 -0
  278. package/src/views/page-editor/EditorCanvas.tsx +127 -0
  279. package/src/views/page-editor/EditorTopBar.tsx +233 -0
  280. package/src/views/page-editor/PagePreview.tsx +96 -0
  281. package/src/views/page-editor/PageSectionEditor.tsx +476 -0
  282. package/src/views/page-editor/PageSettingsModal.tsx +154 -0
  283. package/src/views/page-editor/SectionInspector.tsx +599 -0
  284. package/src/views/page-editor/SectionsPanel.tsx +270 -0
  285. package/src/views/page-editor/ValidationSummary.tsx +53 -0
  286. package/src/views/page-editor/section-icons.ts +33 -0
  287. package/src/views/page-editor/section-types.ts +27 -0
  288. package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
  289. package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
  290. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
  291. package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
  292. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
  293. package/src/views/page-editor/sections/GallerySection.tsx +54 -0
  294. package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
  295. package/src/views/page-editor/sections/MissionSection.tsx +22 -0
  296. package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
  297. package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
  298. package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
  299. package/src/views/page-editor/sections/index.ts +13 -0
  300. package/src/views/page-editor/sections/parts.tsx +113 -0
  301. package/src/views/page-editor/viewports.ts +25 -0
  302. package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
  303. package/src/views/post-editor/PostFieldsModal.tsx +196 -0
  304. package/src/views/post-editor/PostHeader.tsx +87 -0
  305. package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
  306. package/src/views/post-editor/PostPreview.tsx +114 -0
  307. package/src/views/post-editor/PostSectionEditor.tsx +506 -0
  308. package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
@@ -0,0 +1,196 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Modal } from '../../components/ui/Modal.js'
5
+ import { Button } from '../../components/ui/Button.js'
6
+ import type { EditorPost } from '../../lib/post-editor-service.js'
7
+
8
+ interface PostFieldsModalProps {
9
+ open: boolean
10
+ post: EditorPost
11
+ canEdit: boolean
12
+ onClose: () => void
13
+ onSave: (patch: Partial<EditorPost>) => void
14
+ }
15
+
16
+ const LABEL = 'text-foreground mb-1 block text-xs font-medium'
17
+ const INPUT =
18
+ 'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
19
+
20
+ function slugify(value: string): string {
21
+ return value
22
+ .toLowerCase()
23
+ .trim()
24
+ .replace(/[^a-z0-9]+/g, '-')
25
+ .replace(/^-+|-+$/g, '')
26
+ }
27
+
28
+ /** Edits post-level fields: title, slug, excerpt, featured image, category, body, SEO. */
29
+ export function PostFieldsModal({ open, post, canEdit, onClose, onSave }: PostFieldsModalProps) {
30
+ const [title, setTitle] = useState(post.title)
31
+ const [slug, setSlug] = useState(post.slug)
32
+ const [excerpt, setExcerpt] = useState(post.excerpt)
33
+ const [featuredImage, setFeaturedImage] = useState(post.featuredImage)
34
+ const [category, setCategory] = useState(post.category)
35
+ const [body, setBody] = useState(post.body)
36
+ const [seoTitle, setSeoTitle] = useState(post.seoTitle)
37
+ const [seoDescription, setSeoDescription] = useState(post.seoDescription)
38
+ const [slugTouched, setSlugTouched] = useState(Boolean(post.slug))
39
+
40
+ useEffect(() => {
41
+ if (!open) return
42
+ setTitle(post.title)
43
+ setSlug(post.slug)
44
+ setExcerpt(post.excerpt)
45
+ setFeaturedImage(post.featuredImage)
46
+ setCategory(post.category)
47
+ setBody(post.body)
48
+ setSeoTitle(post.seoTitle)
49
+ setSeoDescription(post.seoDescription)
50
+ setSlugTouched(Boolean(post.slug))
51
+ }, [open, post])
52
+
53
+ function handleTitle(next: string) {
54
+ setTitle(next)
55
+ if (!slugTouched) setSlug(slugify(next))
56
+ }
57
+
58
+ function handleSave() {
59
+ onSave({ title, slug, excerpt, featuredImage, category, body, seoTitle, seoDescription })
60
+ onClose()
61
+ }
62
+
63
+ return (
64
+ <Modal
65
+ open={open}
66
+ onClose={onClose}
67
+ title="Post details"
68
+ actions={
69
+ <>
70
+ <Button variant="ghost" onClick={onClose}>
71
+ Cancel
72
+ </Button>
73
+ <Button variant="primary" onClick={handleSave} disabled={!canEdit}>
74
+ Done
75
+ </Button>
76
+ </>
77
+ }
78
+ >
79
+ <div className="space-y-3">
80
+ <div>
81
+ <label className={LABEL} htmlFor="pf-title">
82
+ Title <span className="text-destructive">*</span>
83
+ </label>
84
+ <input
85
+ id="pf-title"
86
+ className={INPUT}
87
+ value={title}
88
+ disabled={!canEdit}
89
+ onChange={(e) => handleTitle(e.target.value)}
90
+ />
91
+ </div>
92
+ <div className="grid grid-cols-2 gap-3">
93
+ <div>
94
+ <label className={LABEL} htmlFor="pf-slug">
95
+ Slug <span className="text-destructive">*</span>
96
+ </label>
97
+ <input
98
+ id="pf-slug"
99
+ className={INPUT}
100
+ value={slug}
101
+ disabled={!canEdit}
102
+ onChange={(e) => {
103
+ setSlugTouched(true)
104
+ setSlug(slugify(e.target.value))
105
+ }}
106
+ />
107
+ </div>
108
+ <div>
109
+ <label className={LABEL} htmlFor="pf-category">
110
+ Category
111
+ </label>
112
+ <input
113
+ id="pf-category"
114
+ className={INPUT}
115
+ value={category}
116
+ disabled={!canEdit}
117
+ placeholder="e.g. Product"
118
+ onChange={(e) => setCategory(e.target.value)}
119
+ />
120
+ </div>
121
+ </div>
122
+ <div>
123
+ <label className={LABEL} htmlFor="pf-excerpt">
124
+ Excerpt
125
+ </label>
126
+ <textarea
127
+ id="pf-excerpt"
128
+ className={`${INPUT} min-h-[60px] resize-y`}
129
+ value={excerpt}
130
+ disabled={!canEdit}
131
+ placeholder="A short summary shown in listings and the header."
132
+ onChange={(e) => setExcerpt(e.target.value)}
133
+ />
134
+ </div>
135
+ <div>
136
+ <label className={LABEL} htmlFor="pf-image">
137
+ Featured image URL
138
+ </label>
139
+ <input
140
+ id="pf-image"
141
+ className={INPUT}
142
+ value={featuredImage}
143
+ disabled={!canEdit}
144
+ placeholder="https://…"
145
+ onChange={(e) => setFeaturedImage(e.target.value)}
146
+ />
147
+ {featuredImage && (
148
+ <div className="border-border bg-muted mt-2 overflow-hidden rounded-md border">
149
+ <img src={featuredImage} alt="" className="h-28 w-full object-cover" />
150
+ </div>
151
+ )}
152
+ </div>
153
+ <div>
154
+ <label className={LABEL} htmlFor="pf-body">
155
+ Body
156
+ </label>
157
+ <textarea
158
+ id="pf-body"
159
+ className={`${INPUT} min-h-[120px] resize-y`}
160
+ value={body}
161
+ disabled={!canEdit}
162
+ placeholder="The main post content. Shown by the Article Body section."
163
+ onChange={(e) => setBody(e.target.value)}
164
+ />
165
+ <p className="text-muted-foreground mt-1 text-xs">
166
+ Rendered by the Article Body section when its source is set to the post Body field.
167
+ </p>
168
+ </div>
169
+ <div>
170
+ <label className={LABEL} htmlFor="pf-seo-title">
171
+ SEO title
172
+ </label>
173
+ <input
174
+ id="pf-seo-title"
175
+ className={INPUT}
176
+ value={seoTitle}
177
+ disabled={!canEdit}
178
+ onChange={(e) => setSeoTitle(e.target.value)}
179
+ />
180
+ </div>
181
+ <div>
182
+ <label className={LABEL} htmlFor="pf-seo-desc">
183
+ SEO description
184
+ </label>
185
+ <textarea
186
+ id="pf-seo-desc"
187
+ className={`${INPUT} min-h-[60px] resize-y`}
188
+ value={seoDescription}
189
+ disabled={!canEdit}
190
+ onChange={(e) => setSeoDescription(e.target.value)}
191
+ />
192
+ </div>
193
+ </div>
194
+ </Modal>
195
+ )
196
+ }
@@ -0,0 +1,87 @@
1
+ import type { PostHeaderConfig } from '../page-editor/section-types.js'
2
+ import type { PostRenderContext } from '../page-editor/sections/index.js'
3
+
4
+ interface PostHeaderProps {
5
+ config: PostHeaderConfig
6
+ context: PostRenderContext
7
+ }
8
+
9
+ function formatDate(value?: string | null): string {
10
+ if (!value) return ''
11
+ const d = new Date(value)
12
+ if (Number.isNaN(d.getTime())) return ''
13
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
14
+ }
15
+
16
+ /**
17
+ * Renders a post's header region from the template's design config plus the
18
+ * post's own field values. Uses the same fixed website palette as the
19
+ * section renderers (see sections/parts.tsx) so the editor canvas and the
20
+ * public preview match.
21
+ */
22
+ export function PostHeader({ config, context }: PostHeaderProps) {
23
+ const title = context.title || 'Untitled post'
24
+ const date = config.showDate ? formatDate(context.publishDate) : ''
25
+ const overlay =
26
+ config.layout === 'overlay' && config.showFeaturedImage && context.featuredImageUrl
27
+ const align = config.layout === 'centered' ? 'text-center mx-auto items-center' : 'text-left'
28
+
29
+ const meta = (
30
+ <div
31
+ className={`mt-4 flex flex-wrap gap-x-3 gap-y-1 text-sm ${
32
+ config.layout === 'centered' ? 'justify-center' : ''
33
+ } ${overlay ? 'text-white/80' : 'text-zinc-500'}`}
34
+ >
35
+ {config.showCategory && context.category && (
36
+ <span className="font-medium text-violet-500">{context.category}</span>
37
+ )}
38
+ {config.showAuthor && <span>{context.author?.name || 'Author name'}</span>}
39
+ {date && <span>{date}</span>}
40
+ {config.showReadingTime && <span>5 min read</span>}
41
+ </div>
42
+ )
43
+
44
+ if (overlay) {
45
+ return (
46
+ <header className="relative">
47
+ <img src={context.featuredImageUrl} alt="" className="h-[360px] w-full object-cover" />
48
+ <div className="absolute inset-0 bg-gradient-to-t from-black/70 to-black/10" aria-hidden />
49
+ <div className="absolute inset-x-0 bottom-0 px-6 pb-8">
50
+ <div className={`mx-auto flex max-w-3xl flex-col ${align}`}>
51
+ <h1 className="text-3xl font-semibold tracking-tight text-white sm:text-4xl">
52
+ {title}
53
+ </h1>
54
+ {config.showExcerpt && context.excerpt && (
55
+ <p className="mt-3 max-w-2xl text-lg text-white/90">{context.excerpt}</p>
56
+ )}
57
+ {meta}
58
+ </div>
59
+ </div>
60
+ </header>
61
+ )
62
+ }
63
+
64
+ return (
65
+ <header className="bg-white px-6 pt-12 pb-2">
66
+ <div
67
+ className={`flex max-w-3xl flex-col ${align} ${config.layout === 'centered' ? 'mx-auto' : ''}`}
68
+ >
69
+ {config.showCategory && context.category && config.layout !== 'centered' && null}
70
+ <h1 className="text-3xl font-semibold tracking-tight text-zinc-900 sm:text-4xl">{title}</h1>
71
+ {config.showExcerpt && context.excerpt && (
72
+ <p className="mt-3 max-w-2xl text-lg text-zinc-600">{context.excerpt}</p>
73
+ )}
74
+ {meta}
75
+ </div>
76
+ {config.showFeaturedImage && context.featuredImageUrl && (
77
+ <div className="mx-auto mt-8 max-w-4xl">
78
+ <img
79
+ src={context.featuredImageUrl}
80
+ alt=""
81
+ className="aspect-[16/9] w-full rounded-xl object-cover"
82
+ />
83
+ </div>
84
+ )}
85
+ </header>
86
+ )
87
+ }
@@ -0,0 +1,107 @@
1
+ 'use client'
2
+
3
+ import { X } from 'lucide-react'
4
+ import type { PostHeaderConfig } from '../../lib/post-editor-service.js'
5
+
6
+ interface PostHeaderPanelProps {
7
+ header: PostHeaderConfig
8
+ canEdit: boolean
9
+ onClose: () => void
10
+ onChange: (patch: Partial<PostHeaderConfig>) => void
11
+ }
12
+
13
+ const LABEL = 'text-foreground mb-1 block text-xs font-medium'
14
+ const INPUT =
15
+ 'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
16
+
17
+ const TOGGLES: Array<{ key: keyof PostHeaderConfig; label: string }> = [
18
+ { key: 'showFeaturedImage', label: 'Featured image' },
19
+ { key: 'showExcerpt', label: 'Excerpt' },
20
+ { key: 'showAuthor', label: 'Author' },
21
+ { key: 'showDate', label: 'Date' },
22
+ { key: 'showCategory', label: 'Category' },
23
+ { key: 'showReadingTime', label: 'Reading time' },
24
+ ]
25
+
26
+ /** Inspector panel for the post header layout (template-level design config). */
27
+ export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHeaderPanelProps) {
28
+ return (
29
+ <aside
30
+ role="dialog"
31
+ aria-label="Edit post header"
32
+ className="bg-card border-border flex h-full w-80 shrink-0 flex-col border-l"
33
+ >
34
+ <div className="border-border flex items-center justify-between border-b px-4 py-3">
35
+ <div className="min-w-0">
36
+ <h2 className="text-foreground truncate text-sm font-medium">Post header</h2>
37
+ <p className="text-muted-foreground text-xs">Layout for every post of this type</p>
38
+ </div>
39
+ <button
40
+ type="button"
41
+ onClick={onClose}
42
+ aria-label="Close header panel"
43
+ className="text-muted-foreground hover:text-foreground hover:bg-accent rounded p-1"
44
+ >
45
+ <X className="h-4 w-4" aria-hidden />
46
+ </button>
47
+ </div>
48
+
49
+ <div className="flex-1 space-y-6 overflow-y-auto p-4">
50
+ <fieldset disabled={!canEdit} className="space-y-3">
51
+ <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
52
+ Layout
53
+ </legend>
54
+ <div>
55
+ <label className={LABEL} htmlFor="ph-layout">
56
+ Header style
57
+ </label>
58
+ <select
59
+ id="ph-layout"
60
+ className={INPUT}
61
+ value={header.layout}
62
+ onChange={(e) => onChange({ layout: e.target.value as PostHeaderConfig['layout'] })}
63
+ >
64
+ <option value="centered">Centered</option>
65
+ <option value="left">Left-aligned</option>
66
+ <option value="overlay">Image overlay</option>
67
+ </select>
68
+ </div>
69
+ </fieldset>
70
+
71
+ <fieldset disabled={!canEdit} className="space-y-2">
72
+ <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
73
+ Show in header
74
+ </legend>
75
+ {TOGGLES.map((t) => {
76
+ const value = header[t.key] === true
77
+ return (
78
+ <button
79
+ key={t.key}
80
+ type="button"
81
+ role="switch"
82
+ aria-checked={value}
83
+ disabled={!canEdit}
84
+ onClick={() => onChange({ [t.key]: !value } as Partial<PostHeaderConfig>)}
85
+ className="border-border text-foreground hover:bg-accent flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm disabled:opacity-50"
86
+ >
87
+ <span>{t.label}</span>
88
+ <span
89
+ aria-hidden
90
+ className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${
91
+ value ? 'bg-primary' : 'bg-muted'
92
+ }`}
93
+ >
94
+ <span
95
+ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
96
+ value ? 'translate-x-4' : 'translate-x-0.5'
97
+ }`}
98
+ />
99
+ </span>
100
+ </button>
101
+ )
102
+ })}
103
+ </fieldset>
104
+ </div>
105
+ </aside>
106
+ )
107
+ }
@@ -0,0 +1,114 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { AlertTriangle, Loader2, Pencil, X } from 'lucide-react'
5
+ import {
6
+ fetchPostForEditor,
7
+ fetchTemplateForEditor,
8
+ type EditorPost,
9
+ type PostHeaderConfig,
10
+ } from '../../lib/post-editor-service.js'
11
+ import { defaultPostHeader } from '@actuate-media/cms-core/page-sections'
12
+ import { SectionRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
13
+ import { PostHeader } from './PostHeader.js'
14
+
15
+ interface PostPreviewProps {
16
+ postType: string
17
+ documentId: string
18
+ session?: any
19
+ onNavigate: (path: string) => void
20
+ }
21
+
22
+ /**
23
+ * Chrome-free render of a post using the same header + section renderers as
24
+ * the editor canvas, so the preview reflects what ships. Only visible
25
+ * sections render (hidden ones are omitted, like the public page).
26
+ */
27
+ export function PostPreview({ postType, documentId, session, onNavigate }: PostPreviewProps) {
28
+ const [post, setPost] = useState<EditorPost | null>(null)
29
+ const [header, setHeader] = useState<PostHeaderConfig>(defaultPostHeader())
30
+ const [loading, setLoading] = useState(true)
31
+ const [error, setError] = useState<string | null>(null)
32
+
33
+ useEffect(() => {
34
+ let cancelled = false
35
+ setLoading(true)
36
+ fetchTemplateForEditor(postType)
37
+ .then((t) => !cancelled && setHeader(t.header))
38
+ .catch(() => {
39
+ /* default header */
40
+ })
41
+ fetchPostForEditor(postType, documentId)
42
+ .then((p) => !cancelled && setPost(p))
43
+ .catch((err) => !cancelled && setError(err instanceof Error ? err.message : 'Failed to load'))
44
+ .finally(() => !cancelled && setLoading(false))
45
+ return () => {
46
+ cancelled = true
47
+ }
48
+ }, [postType, documentId])
49
+
50
+ if (loading) {
51
+ return (
52
+ <div className="bg-background flex h-full items-center justify-center">
53
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
54
+ <span className="sr-only">Loading preview…</span>
55
+ </div>
56
+ )
57
+ }
58
+
59
+ if (error || !post) {
60
+ return (
61
+ <div className="bg-background flex h-full items-center justify-center p-6">
62
+ <div className="border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4">
63
+ <AlertTriangle className="text-destructive shrink-0" size={20} aria-hidden />
64
+ <p className="text-foreground text-sm font-medium">{error ?? 'Post not found.'}</p>
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ const context: PostRenderContext = {
71
+ title: post.title,
72
+ excerpt: post.excerpt,
73
+ body: post.body,
74
+ featuredImageUrl: post.featuredImage,
75
+ publishDate: post.publishDate ?? post.updatedAt ?? null,
76
+ category: post.category,
77
+ author: { name: session?.user?.name || session?.user?.email },
78
+ }
79
+ const visible = post.sections.filter((s) => s.visible)
80
+
81
+ return (
82
+ <div className="flex h-full flex-col">
83
+ {/* Slim preview bar. This view opens in its own browser tab, so
84
+ actions are tab-scoped. */}
85
+ <div className="bg-card border-border flex h-12 shrink-0 items-center justify-between border-b px-4">
86
+ <button
87
+ type="button"
88
+ onClick={() => window.close()}
89
+ className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm"
90
+ aria-label="Close preview tab"
91
+ >
92
+ <X className="h-4 w-4" aria-hidden />
93
+ Close
94
+ </button>
95
+ <span className="text-muted-foreground text-xs">Preview · {post.title || 'Untitled'}</span>
96
+ <button
97
+ type="button"
98
+ onClick={() => onNavigate(`/posts/${postType}/${documentId}/edit`)}
99
+ className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium"
100
+ >
101
+ <Pencil className="h-3.5 w-3.5" aria-hidden />
102
+ Open editor
103
+ </button>
104
+ </div>
105
+
106
+ <div className="flex-1 overflow-auto bg-white">
107
+ <PostHeader config={header} context={context} />
108
+ {visible.map((section) => (
109
+ <SectionRenderer key={section.id} section={section} context={context} />
110
+ ))}
111
+ </div>
112
+ </div>
113
+ )
114
+ }