@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,144 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { AlertTriangle, ArrowLeft, Layout, Loader2, Save } from 'lucide-react';
5
+ import { toast } from 'sonner';
6
+ import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js';
7
+ import { ErrorBoundary } from '../../components/ErrorBoundary.js';
8
+ import { addSection, createSection, duplicateSection, fetchTemplateForEditor, reorderSections, removeSection, saveTemplate, toggleSectionVisibility, updateSectionContent, updateSectionMeta, updateSectionSettings, validateTemplate, } from '../../lib/post-editor-service.js';
9
+ import { DEFAULT_VIEWPORT } from '../page-editor/viewports.js';
10
+ import { SectionsPanel } from '../page-editor/SectionsPanel.js';
11
+ import { SectionInspector } from '../page-editor/SectionInspector.js';
12
+ import { AddSectionModal } from '../page-editor/AddSectionModal.js';
13
+ import { PostEditorCanvas } from './PostEditorCanvas.js';
14
+ import { PostHeaderPanel } from './PostHeaderPanel.js';
15
+ function deriveCanEdit(session) {
16
+ const role = session?.user?.role;
17
+ if (!role)
18
+ return true;
19
+ return ['ADMIN', 'EDITOR'].includes(role);
20
+ }
21
+ const SAMPLE_CONTEXT = {
22
+ title: 'Sample Post Title',
23
+ excerpt: 'A short summary that previews how the header excerpt will look.',
24
+ body: 'This is sample body copy.\n\nIt shows how the Article Body section renders the post body once an author writes a post with this template.',
25
+ publishDate: new Date().toISOString(),
26
+ category: 'Category',
27
+ author: { name: 'Author Name', bio: 'A short example author bio for the preview.' },
28
+ };
29
+ export function PostTemplateEditor({ postType, config, session, onNavigate, }) {
30
+ const [template, setTemplate] = useState(null);
31
+ const [loading, setLoading] = useState(true);
32
+ const [loadError, setLoadError] = useState(null);
33
+ // selection: a section id, the special 'header' panel, or nothing.
34
+ const [selected, setSelected] = useState(null);
35
+ const [viewport, setViewport] = useState(DEFAULT_VIEWPORT);
36
+ const [dirty, setDirty] = useState(false);
37
+ const [saving, setSaving] = useState(false);
38
+ const [addOpen, setAddOpen] = useState(false);
39
+ const [deleteTargetId, setDeleteTargetId] = useState(null);
40
+ const [leaveTarget, setLeaveTarget] = useState(null);
41
+ const canEdit = useMemo(() => deriveCanEdit(session), [session]);
42
+ const typeLabel = useMemo(() => {
43
+ const col = config?.collections?.[postType];
44
+ return col?.labels?.singular ?? postType;
45
+ }, [config, postType]);
46
+ useEffect(() => {
47
+ let cancelled = false;
48
+ setLoading(true);
49
+ setLoadError(null);
50
+ fetchTemplateForEditor(postType)
51
+ .then((t) => {
52
+ if (!cancelled)
53
+ setTemplate(t);
54
+ })
55
+ .catch((err) => {
56
+ if (!cancelled)
57
+ setLoadError(err instanceof Error ? err.message : 'Failed to load template');
58
+ })
59
+ .finally(() => {
60
+ if (!cancelled)
61
+ setLoading(false);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }, [postType]);
67
+ useEffect(() => {
68
+ if (!dirty)
69
+ return;
70
+ function onBeforeUnload(e) {
71
+ e.preventDefault();
72
+ e.returnValue = '';
73
+ }
74
+ window.addEventListener('beforeunload', onBeforeUnload);
75
+ return () => window.removeEventListener('beforeunload', onBeforeUnload);
76
+ }, [dirty]);
77
+ const mutateSections = useCallback((fn) => {
78
+ setTemplate((prev) => (prev ? { ...prev, sections: fn(prev.sections) } : prev));
79
+ setDirty(true);
80
+ }, []);
81
+ const handleHeaderChange = useCallback((patch) => {
82
+ setTemplate((prev) => (prev ? { ...prev, header: { ...prev.header, ...patch } } : prev));
83
+ setDirty(true);
84
+ }, []);
85
+ const handleAdd = useCallback((type) => {
86
+ const section = createSection(type);
87
+ mutateSections((s) => addSection(s, section));
88
+ setSelected({ kind: 'section', id: section.id });
89
+ setAddOpen(false);
90
+ }, [mutateSections]);
91
+ const selectedSectionId = selected?.kind === 'section' ? selected.id : null;
92
+ const handleSave = useCallback(async () => {
93
+ if (!template)
94
+ return;
95
+ const result = validateTemplate(template);
96
+ if (result.errors.length > 0) {
97
+ toast.error(result.errors[0]);
98
+ return;
99
+ }
100
+ setSaving(true);
101
+ try {
102
+ const saved = await saveTemplate(template);
103
+ setTemplate(saved);
104
+ setDirty(false);
105
+ toast.success('Template saved');
106
+ }
107
+ catch (err) {
108
+ toast.error(err instanceof Error ? err.message : 'Failed to save template');
109
+ }
110
+ finally {
111
+ setSaving(false);
112
+ }
113
+ }, [template]);
114
+ const guardedNavigate = useCallback((path) => {
115
+ if (dirty)
116
+ setLeaveTarget(path);
117
+ else
118
+ onNavigate(path);
119
+ }, [dirty, onNavigate]);
120
+ if (loading) {
121
+ return (_jsxs("div", { className: "bg-background flex h-full items-center justify-center", children: [_jsx(Loader2, { className: "text-muted-foreground h-6 w-6 animate-spin", "aria-hidden": true }), _jsx("span", { className: "sr-only", children: "Loading template editor\u2026" })] }));
122
+ }
123
+ if (loadError || !template) {
124
+ return (_jsx("div", { className: "bg-background flex h-full items-center justify-center p-6", children: _jsxs("div", { className: "border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4", children: [_jsx(AlertTriangle, { className: "text-destructive shrink-0", size: 20, "aria-hidden": true }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: "Failed to load template" }), _jsx("p", { className: "text-muted-foreground mt-1 text-sm", children: loadError ?? 'Not found.' }), _jsx("button", { type: "button", onClick: () => onNavigate('/posts/types'), className: "text-primary mt-2 text-sm font-medium hover:underline", children: "Back to Post Types" })] })] }) }));
125
+ }
126
+ const selectedSection = template.sections.find((s) => s.id === selectedSectionId) ?? null;
127
+ return (_jsxs(ErrorBoundary, { children: [_jsxs("div", { className: "bg-background flex h-full flex-col overflow-hidden", children: [_jsxs("header", { className: "bg-card border-border flex h-14 shrink-0 items-center justify-between gap-4 border-b px-4", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex min-w-0 items-center gap-1.5 text-sm", children: [_jsxs("button", { type: "button", onClick: () => guardedNavigate('/posts/types'), className: "text-muted-foreground hover:text-foreground inline-flex shrink-0 items-center gap-1.5", children: [_jsx(ArrowLeft, { className: "h-4 w-4", "aria-hidden": true }), "Post Types"] }), _jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "/" }), _jsxs("span", { className: "text-foreground truncate font-medium", children: [typeLabel, " Template"] }), dirty && (_jsx("span", { className: "text-muted-foreground ml-2 text-xs", "aria-live": "polite", children: "Unsaved changes" }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("button", { type: "button", onClick: () => setSelected({ kind: 'header' }), className: `inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors ${selected?.kind === 'header'
128
+ ? 'border-primary bg-accent text-foreground'
129
+ : 'border-border text-foreground hover:bg-accent'}`, children: [_jsx(Layout, { className: "h-4 w-4", "aria-hidden": true }), "Header"] }), _jsxs("button", { type: "button", onClick: handleSave, disabled: !canEdit || saving || !dirty, className: "bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50", children: [saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": true })) : (_jsx(Save, { className: "h-3.5 w-3.5", "aria-hidden": true })), "Save"] })] })] }), _jsxs("div", { className: "flex flex-1 overflow-hidden", children: [_jsx(SectionsPanel, { sections: template.sections, selectedId: selectedSectionId, canEdit: canEdit, onSelect: (id) => setSelected({ kind: 'section', id }), onReorder: (ids) => mutateSections((s) => reorderSections(s, ids)), onToggleVisibility: (id) => mutateSections((s) => toggleSectionVisibility(s, id)), onDuplicate: (id) => mutateSections((s) => duplicateSection(s, id)), onRequestDelete: (id) => setDeleteTargetId(id), onAdd: () => setAddOpen(true) }), _jsx("div", { className: "flex-1 overflow-hidden", children: _jsx(PostEditorCanvas, { sections: template.sections, header: template.header, context: SAMPLE_CONTEXT, selectedId: selectedSectionId, viewport: viewport, onSelect: (id) => setSelected({ kind: 'section', id }), onAddSection: () => setAddOpen(true) }) }), selected?.kind === 'header' && (_jsx(PostHeaderPanel, { header: template.header, canEdit: canEdit, onClose: () => setSelected(null), onChange: handleHeaderChange })), selectedSection && (_jsx(SectionInspector, { section: selectedSection, canEdit: canEdit, onClose: () => setSelected(null), onContentChange: (patch) => mutateSections((s) => updateSectionContent(s, selectedSection.id, patch)), onSettingsChange: (patch) => mutateSections((s) => updateSectionSettings(s, selectedSection.id, patch)), onMetaChange: (patch) => mutateSections((s) => updateSectionMeta(s, selectedSection.id, patch)), onToggleVisibility: () => mutateSections((s) => toggleSectionVisibility(s, selectedSection.id)) }))] })] }), _jsx(AddSectionModal, { open: addOpen, scope: "post", onClose: () => setAddOpen(false), onAdd: handleAdd }), _jsx(ConfirmDialog, { open: deleteTargetId !== null, onClose: () => setDeleteTargetId(null), onConfirm: () => {
130
+ if (deleteTargetId) {
131
+ mutateSections((s) => removeSection(s, deleteTargetId));
132
+ if (selectedSectionId === deleteTargetId)
133
+ setSelected(null);
134
+ }
135
+ setDeleteTargetId(null);
136
+ }, title: "Delete section?", description: "This removes the section from the template. You can undo by leaving without saving.", confirmLabel: "Delete", destructive: true }), _jsx(ConfirmDialog, { open: leaveTarget !== null, onClose: () => setLeaveTarget(null), onConfirm: () => {
137
+ const target = leaveTarget;
138
+ setLeaveTarget(null);
139
+ setDirty(false);
140
+ if (target)
141
+ onNavigate(target);
142
+ }, title: "Discard unsaved changes?", description: "You have unsaved changes that will be lost if you leave now.", confirmLabel: "Leave", cancelLabel: "Stay", destructive: true })] }));
143
+ }
144
+ //# sourceMappingURL=PostTemplateEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostTemplateEditor.js","sourceRoot":"","sources":["../../../src/views/post-editor/PostTemplateEditor.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-admin",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",
@@ -87,7 +87,7 @@
87
87
  "tailwindcss": "^4.0.0",
88
88
  "typescript": "^5.7.0",
89
89
  "vitest": "^3.0.0",
90
- "@actuate-media/cms-core": "0.25.1",
90
+ "@actuate-media/cms-core": "0.27.0",
91
91
  "@actuate-media/component-blocks": "0.2.0"
92
92
  },
93
93
  "scripts": {
package/src/AdminRoot.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useState, useMemo, useEffect, useRef } from 'react'
3
+ import { useState, useMemo, useEffect, useRef, useCallback } from 'react'
4
+ import { toast } from 'sonner'
4
5
  import { Layout } from './layout/Layout.js'
5
6
  import { useAdminRouter } from './router/index.js'
6
7
  import { ensureCsrfToken } from './lib/api.js'
@@ -26,8 +27,20 @@ import { ThemeProvider } from './components/ThemeProvider.js'
26
27
  import { LocaleProvider } from './components/LocaleProvider.js'
27
28
  import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js'
28
29
  import { PageBuilder } from './views/page-builder/PageBuilder.js'
30
+ import { PageSectionEditor } from './views/page-editor/PageSectionEditor.js'
31
+ import { PagePreview } from './views/page-editor/PagePreview.js'
29
32
  import { SavedSections } from './views/page-builder/SavedSections.js'
30
33
  import { PageTemplates } from './views/page-builder/PageTemplates.js'
34
+ import { Widgets } from './views/page-builder/Widgets.js'
35
+ import { PagesListView } from './views/Pages/PagesListView.js'
36
+ import { PageTagManager } from './views/Pages/PageTagManager.js'
37
+ import { PostsListView } from './views/Posts/PostsListView.js'
38
+ import { PostTypesView } from './views/Posts/PostTypesView.js'
39
+ import { NewPostPicker } from './views/Posts/NewPostPicker.js'
40
+ import { NewPostTypeInstructions } from './views/Posts/NewPostTypeInstructions.js'
41
+ import { PostSectionEditor } from './views/post-editor/PostSectionEditor.js'
42
+ import { PostTemplateEditor } from './views/post-editor/PostTemplateEditor.js'
43
+ import { PostPreview } from './views/post-editor/PostPreview.js'
31
44
 
32
45
  export interface AdminRootProps {
33
46
  config: any
@@ -58,9 +71,45 @@ function AdminShell({
58
71
  onLogin,
59
72
  captchaConfig,
60
73
  }: AdminRootProps) {
61
- const { currentPath, navigate, matchRoute } = useAdminRouter(basePath, initialPath)
74
+ const { currentPath, navigate, matchRoute, buildHref } = useAdminRouter(basePath, initialPath)
62
75
  const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false)
63
76
 
77
+ // Open a preview route in a new browser tab so it behaves like a normal,
78
+ // shareable page (its own URL, no admin chrome — see the standalone branch
79
+ // below). The blank tab is opened synchronously inside the click gesture so
80
+ // popup blockers permit it; `prepare` (e.g. saving an unsaved draft) runs
81
+ // before we point the tab at the URL, guaranteeing the preview reflects the
82
+ // latest saved content. If `prepare` rejects, the tab is closed.
83
+ const openPreview = useCallback(
84
+ (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => {
85
+ if (typeof window === 'undefined') return
86
+ const tab = window.open('about:blank', '_blank')
87
+ if (!tab) {
88
+ toast.error('Enable pop-ups for this site to open the preview in a new tab.')
89
+ return
90
+ }
91
+ const go = (ok: boolean | void) => {
92
+ if (ok === false) {
93
+ tab?.close()
94
+ return
95
+ }
96
+ if (tab && !tab.closed) {
97
+ // Sever the opener link for safety, then load the real URL.
98
+ try {
99
+ tab.opener = null
100
+ } catch {
101
+ /* cross-origin guard — never throws for same-origin admin */
102
+ }
103
+ tab.location.replace(buildHref(path))
104
+ }
105
+ }
106
+ Promise.resolve(prepare?.())
107
+ .then(go)
108
+ .catch(() => tab?.close())
109
+ },
110
+ [buildHref],
111
+ )
112
+
64
113
  useBranding(config)
65
114
  useAdminPageTitle(config, currentPath)
66
115
 
@@ -178,6 +227,142 @@ function AdminShell({
178
227
  return <MediaBrowser onNavigate={navigate} />
179
228
  }
180
229
 
230
+ // ─── Posts area ────────────────────────────────────────────────
231
+ // These take priority over the per-collection `/<slug>` matcher
232
+ // below so a consumer-defined `posts` collection can't accidentally
233
+ // shadow the aggregate Posts view.
234
+ if (matchRoute('/posts/types/new')) {
235
+ return <NewPostTypeInstructions onNavigate={navigate} />
236
+ }
237
+ // Per-type template editor — must match before `/posts/types/:slug/edit`
238
+ // is irrelevant (distinct last segment) but stays grouped with the type
239
+ // routes so it can't be shadowed by the generic `/posts/:type/*` block.
240
+ const postTypeTemplate = matchRoute('/posts/types/:slug/template')
241
+ if (postTypeTemplate?.slug) {
242
+ return (
243
+ <PostTemplateEditor
244
+ postType={postTypeTemplate.slug}
245
+ config={config}
246
+ session={session}
247
+ onNavigate={navigate}
248
+ />
249
+ )
250
+ }
251
+ const postTypeEdit = matchRoute('/posts/types/:slug/edit')
252
+ if (postTypeEdit?.slug) {
253
+ // V1 falls through to the per-collection list view; future
254
+ // PR introduces a dedicated PostTypeEditor with field-level
255
+ // metadata. The list view is the natural per-type management
256
+ // surface today (it already shows posts in that type).
257
+ return (
258
+ <CollectionList collectionSlug={postTypeEdit.slug} config={config} onNavigate={navigate} />
259
+ )
260
+ }
261
+ if (matchRoute('/posts/types')) {
262
+ return <PostTypesView onNavigate={navigate} />
263
+ }
264
+ if (matchRoute('/posts/new')) {
265
+ return <NewPostPicker onNavigate={navigate} />
266
+ }
267
+ if (matchRoute('/posts')) {
268
+ return <PostsListView onNavigate={navigate} />
269
+ }
270
+ // Section-based Post editor. These `/posts/:type/*` patterns are tested
271
+ // AFTER every `/posts/types/*` route above so `:type` cannot swallow the
272
+ // reserved "types" segment.
273
+ const postNew = matchRoute('/posts/:type/new')
274
+ if (postNew?.type) {
275
+ return (
276
+ <PostSectionEditor
277
+ postType={postNew.type}
278
+ config={config}
279
+ session={session}
280
+ onNavigate={navigate}
281
+ onOpenPreview={openPreview}
282
+ />
283
+ )
284
+ }
285
+ const postPreview = matchRoute('/posts/:type/:id/preview')
286
+ if (postPreview?.type && postPreview?.id) {
287
+ return (
288
+ <PostPreview
289
+ postType={postPreview.type}
290
+ documentId={postPreview.id}
291
+ session={session}
292
+ onNavigate={navigate}
293
+ />
294
+ )
295
+ }
296
+ const postEdit = matchRoute('/posts/:type/:id/edit')
297
+ if (postEdit?.type && postEdit?.id) {
298
+ return (
299
+ <PostSectionEditor
300
+ postType={postEdit.type}
301
+ documentId={postEdit.id}
302
+ config={config}
303
+ session={session}
304
+ onNavigate={navigate}
305
+ onOpenPreview={openPreview}
306
+ />
307
+ )
308
+ }
309
+
310
+ // ─── Pages area ────────────────────────────────────────────────
311
+ // Ordering matters: tag routes and /pages/new must be tested before
312
+ // the `/pages/:id` matcher, otherwise `:id` swallows "tags"/"new".
313
+ if (matchRoute('/pages/tags/new')) {
314
+ return <PageTagManager onNavigate={navigate} startCreating />
315
+ }
316
+ const pageTagEdit = matchRoute('/pages/tags/:id/edit')
317
+ if (pageTagEdit?.id) {
318
+ // The manager is a single CRUD surface; deep-linking to a tag just
319
+ // opens the manager (inline edit lives per-row).
320
+ return <PageTagManager onNavigate={navigate} />
321
+ }
322
+ if (matchRoute('/pages/tags')) {
323
+ return <PageTagManager onNavigate={navigate} />
324
+ }
325
+ if (matchRoute('/pages/new')) {
326
+ return (
327
+ <PageSectionEditor
328
+ config={config}
329
+ session={session}
330
+ onNavigate={navigate}
331
+ onOpenPreview={openPreview}
332
+ />
333
+ )
334
+ }
335
+ const pagePreview = matchRoute('/pages/:id/preview')
336
+ if (pagePreview?.id) {
337
+ return <PagePreview documentId={pagePreview.id} onNavigate={navigate} />
338
+ }
339
+ const pageEdit = matchRoute('/pages/:id/edit')
340
+ if (pageEdit?.id) {
341
+ return (
342
+ <PageSectionEditor
343
+ documentId={pageEdit.id}
344
+ config={config}
345
+ session={session}
346
+ onNavigate={navigate}
347
+ onOpenPreview={openPreview}
348
+ />
349
+ )
350
+ }
351
+ const pageEditBare = matchRoute('/pages/:id')
352
+ if (pageEditBare?.id) {
353
+ return (
354
+ <PageSectionEditor
355
+ documentId={pageEditBare.id}
356
+ config={config}
357
+ session={session}
358
+ onNavigate={navigate}
359
+ />
360
+ )
361
+ }
362
+ if (matchRoute('/pages')) {
363
+ return <PagesListView onNavigate={navigate} />
364
+ }
365
+
181
366
  if (matchRoute('/forms/new')) {
182
367
  return <FormEditor onNavigate={navigate} />
183
368
  }
@@ -225,6 +410,10 @@ function AdminShell({
225
410
  return <PageTemplates onNavigate={navigate} />
226
411
  }
227
412
 
413
+ if (matchRoute('/widgets')) {
414
+ return <Widgets onNavigate={navigate} />
415
+ }
416
+
228
417
  if (matchRoute('/users')) {
229
418
  return <Users onNavigate={navigate} />
230
419
  }
@@ -264,10 +453,21 @@ function AdminShell({
264
453
  'script-tags',
265
454
  'saved-sections',
266
455
  'page-templates',
456
+ 'widgets',
267
457
  'users',
268
458
  'settings',
269
459
  'page-builder',
270
460
  'collections',
461
+ // `pages` is reserved so the Pages list/editor/tag routes above
462
+ // own the `/pages/*` namespace and can't be shadowed by a
463
+ // consumer-defined `pages` collection. The per-collection list is
464
+ // still reachable via `/collections/pages`.
465
+ 'pages',
466
+ // `posts` is reserved so the aggregate Posts area is reachable
467
+ // even if a consumer defines a collection literally called
468
+ // `posts`. The per-type collection list is still available via
469
+ // `/collections/posts`.
470
+ 'posts',
271
471
  ])
272
472
 
273
473
  for (const [slug, def] of collectionMap) {
@@ -354,6 +554,17 @@ function AdminShell({
354
554
  )
355
555
  }
356
556
 
557
+ // Preview routes render as standalone pages (no sidebar/header) so a
558
+ // new-tab preview looks like the real published page rather than an
559
+ // embedded admin view. Everything else gets the full admin chrome.
560
+ if (currentPath.endsWith('/preview')) {
561
+ return (
562
+ <div className="bg-background h-full min-h-screen">
563
+ <ErrorBoundary>{renderView()}</ErrorBoundary>
564
+ </div>
565
+ )
566
+ }
567
+
357
568
  return (
358
569
  <Layout config={config} session={session} currentPath={currentPath} onNavigate={navigate}>
359
570
  <ErrorBoundary>{renderView()}</ErrorBoundary>
@@ -0,0 +1,77 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it } from 'vitest'
3
+ import { render, screen } from '@testing-library/react'
4
+ import {
5
+ PostStatusBadge,
6
+ PostTypeBadge,
7
+ SeoScoreIndicator,
8
+ seoScorePalette,
9
+ paletteFor,
10
+ } from '../../components/posts/badges.js'
11
+
12
+ describe('seoScorePalette', () => {
13
+ it('returns gray for null', () => {
14
+ expect(seoScorePalette(null)).toBe('gray')
15
+ })
16
+ it.each([
17
+ [80, 'green'],
18
+ [100, 'green'],
19
+ [79, 'orange'],
20
+ [60, 'orange'],
21
+ [59, 'red'],
22
+ [1, 'red'],
23
+ [0, 'gray'],
24
+ ])('score %i → %s', (input, expected) => {
25
+ expect(seoScorePalette(input)).toBe(expected)
26
+ })
27
+ })
28
+
29
+ describe('paletteFor', () => {
30
+ it('falls back to gray for unknown colors', () => {
31
+ const p = paletteFor('chartreuse')
32
+ expect(p.bg).toContain('muted')
33
+ })
34
+ it('returns gray for null', () => {
35
+ const p = paletteFor(null)
36
+ expect(p.bg).toContain('muted')
37
+ })
38
+ it('returns purple tokens for "purple"', () => {
39
+ const p = paletteFor('purple')
40
+ expect(p.bg).toMatch(/violet/)
41
+ expect(p.border).toMatch(/violet/)
42
+ })
43
+ })
44
+
45
+ describe('SeoScoreIndicator', () => {
46
+ it('renders a dash for missing score', () => {
47
+ render(<SeoScoreIndicator score={null} />)
48
+ expect(screen.getByText('—')).toBeTruthy()
49
+ })
50
+ it('renders the numeric score when present', () => {
51
+ render(<SeoScoreIndicator score={88} />)
52
+ expect(screen.getByText('88')).toBeTruthy()
53
+ })
54
+ })
55
+
56
+ describe('PostStatusBadge', () => {
57
+ it('renders the human label for each status', () => {
58
+ render(<PostStatusBadge status="PUBLISHED" />)
59
+ expect(screen.getByText('Published')).toBeTruthy()
60
+ })
61
+ it('uses an aria-label so screen readers get a clean Status: foo announcement', () => {
62
+ const { container } = render(<PostStatusBadge status="SCHEDULED" />)
63
+ expect(container.querySelector('[aria-label="Status: Scheduled"]')).toBeTruthy()
64
+ })
65
+ })
66
+
67
+ describe('PostTypeBadge', () => {
68
+ it('renders as a static span when no onClick is provided', () => {
69
+ const { container } = render(<PostTypeBadge label="Blog" color="purple" />)
70
+ expect(container.querySelector('span')?.textContent).toBe('Blog')
71
+ expect(container.querySelector('button')).toBeNull()
72
+ })
73
+ it('renders as a button when onClick is provided', () => {
74
+ render(<PostTypeBadge label="Blog" color="purple" onClick={() => {}} />)
75
+ expect(screen.getByRole('button', { name: 'Blog' })).toBeTruthy()
76
+ })
77
+ })