@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
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,143 @@ 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
+ onOpenPreview={openPreview}
360
+ />
361
+ )
362
+ }
363
+ if (matchRoute('/pages')) {
364
+ return <PagesListView onNavigate={navigate} />
365
+ }
366
+
181
367
  if (matchRoute('/forms/new')) {
182
368
  return <FormEditor onNavigate={navigate} />
183
369
  }
@@ -225,6 +411,10 @@ function AdminShell({
225
411
  return <PageTemplates onNavigate={navigate} />
226
412
  }
227
413
 
414
+ if (matchRoute('/widgets')) {
415
+ return <Widgets onNavigate={navigate} />
416
+ }
417
+
228
418
  if (matchRoute('/users')) {
229
419
  return <Users onNavigate={navigate} />
230
420
  }
@@ -264,10 +454,21 @@ function AdminShell({
264
454
  'script-tags',
265
455
  'saved-sections',
266
456
  'page-templates',
457
+ 'widgets',
267
458
  'users',
268
459
  'settings',
269
460
  'page-builder',
270
461
  'collections',
462
+ // `pages` is reserved so the Pages list/editor/tag routes above
463
+ // own the `/pages/*` namespace and can't be shadowed by a
464
+ // consumer-defined `pages` collection. The per-collection list is
465
+ // still reachable via `/collections/pages`.
466
+ 'pages',
467
+ // `posts` is reserved so the aggregate Posts area is reachable
468
+ // even if a consumer defines a collection literally called
469
+ // `posts`. The per-type collection list is still available via
470
+ // `/collections/posts`.
471
+ 'posts',
271
472
  ])
272
473
 
273
474
  for (const [slug, def] of collectionMap) {
@@ -354,6 +555,17 @@ function AdminShell({
354
555
  )
355
556
  }
356
557
 
558
+ // Preview routes render as standalone pages (no sidebar/header) so a
559
+ // new-tab preview looks like the real published page rather than an
560
+ // embedded admin view. Everything else gets the full admin chrome.
561
+ if (currentPath.endsWith('/preview')) {
562
+ return (
563
+ <div className="bg-background h-full min-h-screen">
564
+ <ErrorBoundary>{renderView()}</ErrorBoundary>
565
+ </div>
566
+ )
567
+ }
568
+
357
569
  return (
358
570
  <Layout config={config} session={session} currentPath={currentPath} onNavigate={navigate}>
359
571
  <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
+ })
@@ -0,0 +1,179 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildNavItems } from '../../layout/Sidebar.js'
3
+
4
+ /**
5
+ * Regression coverage for the Posts area sidebar grouping.
6
+ *
7
+ * The reference design shows a single "Posts" parent with two fixed
8
+ * children — All Posts and Post Types — even when the config defines
9
+ * multiple post-type collections (Blog, Case Studies, News, ...). That
10
+ * collapsing is fragile (it's tempting to enumerate each collection
11
+ * as its own sidebar entry) so we lock the contract with explicit
12
+ * tests.
13
+ */
14
+
15
+ describe('buildNavItems', () => {
16
+ it('returns the default tree when no collections are provided', () => {
17
+ const items = buildNavItems({})
18
+ const posts = items.find((i) => i.path === '/posts')
19
+ expect(posts).toBeDefined()
20
+ expect(posts?.children?.map((c) => c.path)).toEqual(['/posts', '/posts/types'])
21
+ // Pages no longer carries children — Saved Sections / Templates moved
22
+ // under the dedicated Templates parent.
23
+ const pages = items.find((i) => i.path === '/pages')
24
+ expect(pages?.children ?? []).toEqual([])
25
+ // Templates is its own item with Saved Sections + Widgets.
26
+ const templates = items.find((i) => i.path === '/page-templates')
27
+ expect(templates?.children?.map((c) => c.path)).toEqual(['/saved-sections', '/widgets'])
28
+ })
29
+
30
+ it('exposes Templates as a top-level Content item with Saved Sections + Widgets', () => {
31
+ const items = buildNavItems({
32
+ collections: {
33
+ pages: { slug: 'pages', labels: { singular: 'Page', plural: 'Pages' }, type: 'page' },
34
+ },
35
+ })
36
+ const templates = items.find((i) => i.path === '/page-templates')
37
+ expect(templates).toBeDefined()
38
+ expect(templates?.group).toBe('Content')
39
+ expect(templates?.children?.map((c) => c.label)).toEqual(['Saved Sections', 'Widgets'])
40
+ // Templates is NOT nested under Pages.
41
+ const pages = items.find((i) => i.path === '/pages')
42
+ expect(pages?.children ?? []).toEqual([])
43
+ })
44
+
45
+ it('collapses every type:post collection under a single Posts parent', () => {
46
+ const items = buildNavItems({
47
+ collections: {
48
+ blog: { slug: 'blog', labels: { singular: 'Blog Post', plural: 'Blog' }, type: 'post' },
49
+ 'case-studies': {
50
+ slug: 'case-studies',
51
+ labels: { singular: 'Case Study', plural: 'Case Studies' },
52
+ type: 'post',
53
+ },
54
+ news: { slug: 'news', labels: { singular: 'News Item', plural: 'News' }, type: 'post' },
55
+ pages: { slug: 'pages', labels: { singular: 'Page', plural: 'Pages' }, type: 'page' },
56
+ },
57
+ })
58
+
59
+ // Exactly one /posts entry, regardless of how many post types exist.
60
+ const postEntries = items.filter((i) => i.path === '/posts')
61
+ expect(postEntries).toHaveLength(1)
62
+
63
+ // Children are still the fixed pair — the per-type collections
64
+ // are NOT enumerated as sidebar siblings.
65
+ const children = postEntries[0]?.children?.map((c) => c.path)
66
+ expect(children).toEqual(['/posts', '/posts/types'])
67
+
68
+ // No per-type slug ended up as a top-level sidebar item.
69
+ const slugPaths = items.map((i) => i.path)
70
+ for (const slug of ['blog', 'case-studies', 'news']) {
71
+ expect(slugPaths).not.toContain(`/${slug}`)
72
+ }
73
+ })
74
+
75
+ it('treats admin.group=Posts (case-insensitive) as a synonym for type=post', () => {
76
+ const items = buildNavItems({
77
+ collections: {
78
+ reviews: {
79
+ slug: 'reviews',
80
+ labels: { singular: 'Review', plural: 'Reviews' },
81
+ // Intentionally NOT type:'post' — relying on the group marker.
82
+ admin: { group: 'posts' },
83
+ },
84
+ },
85
+ })
86
+ expect(items.some((i) => i.path === '/posts')).toBe(true)
87
+ expect(items.some((i) => i.path === '/reviews')).toBe(false)
88
+ })
89
+
90
+ it('omits the Posts parent entirely when no post-type collections exist', () => {
91
+ const items = buildNavItems({
92
+ collections: {
93
+ pages: { slug: 'pages', labels: { singular: 'Page', plural: 'Pages' }, type: 'page' },
94
+ },
95
+ })
96
+ expect(items.some((i) => i.path === '/posts')).toBe(false)
97
+ })
98
+
99
+ it('places Pages above Posts, both under the CONTENT group', () => {
100
+ const items = buildNavItems({
101
+ collections: {
102
+ blog: { slug: 'blog', labels: { singular: 'Blog Post', plural: 'Blog' }, type: 'post' },
103
+ pages: { slug: 'pages', labels: { singular: 'Page', plural: 'Pages' }, type: 'page' },
104
+ },
105
+ })
106
+
107
+ const pagesIdx = items.findIndex((i) => i.path === '/pages')
108
+ const postsIdx = items.findIndex((i) => i.path === '/posts')
109
+ const mediaIdx = items.findIndex((i) => i.path === '/media')
110
+
111
+ // Pages comes before Posts, and both come before the static
112
+ // content destinations.
113
+ expect(pagesIdx).toBeGreaterThanOrEqual(0)
114
+ expect(pagesIdx).toBeLessThan(postsIdx)
115
+ expect(postsIdx).toBeLessThan(mediaIdx)
116
+
117
+ // Both are tagged Content so the single CONTENT header sits above
118
+ // Pages (the first Content item in the tree).
119
+ expect(items[pagesIdx]?.group).toBe('Content')
120
+ expect(items[postsIdx]?.group).toBe('Content')
121
+
122
+ // The first item carrying group 'Content' is Pages — i.e. the
123
+ // CONTENT header renders directly above it.
124
+ const firstContent = items.find((i) => i.group === 'Content')
125
+ expect(firstContent?.path).toBe('/pages')
126
+ })
127
+
128
+ it('defaults: Pages is the first CONTENT item, above Posts', () => {
129
+ const items = buildNavItems({})
130
+ const pagesIdx = items.findIndex((i) => i.path === '/pages')
131
+ const postsIdx = items.findIndex((i) => i.path === '/posts')
132
+ expect(items[pagesIdx]?.group).toBe('Content')
133
+ expect(items[postsIdx]?.group).toBe('Content')
134
+ expect(pagesIdx).toBeLessThan(postsIdx)
135
+ expect(items.find((i) => i.group === 'Content')?.path).toBe('/pages')
136
+ })
137
+
138
+ it('renders an admin.group=Content collection (Menus) as its own Content item', () => {
139
+ const items = buildNavItems({
140
+ collections: {
141
+ pages: { slug: 'pages', labels: { singular: 'Page', plural: 'Pages' }, type: 'page' },
142
+ navigations: {
143
+ slug: 'navigations',
144
+ labels: { singular: 'Menu', plural: 'Menus' },
145
+ admin: { group: 'Content', icon: 'navigation' },
146
+ },
147
+ },
148
+ })
149
+
150
+ const menus = items.find((i) => i.path === '/navigations')
151
+ expect(menus).toBeDefined()
152
+ expect(menus?.label).toBe('Menus')
153
+ expect(menus?.group).toBe('Content')
154
+ // It is a flat Content entry, not a child of Posts/Templates/Pages.
155
+ expect(menus?.children).toBeUndefined()
156
+ // Lands after the static Content destinations, before Settings.
157
+ const menusIdx = items.findIndex((i) => i.path === '/navigations')
158
+ const scriptIdx = items.findIndex((i) => i.path === '/script-tags')
159
+ const usersIdx = items.findIndex((i) => i.path === '/users')
160
+ expect(menusIdx).toBeGreaterThan(scriptIdx)
161
+ expect(menusIdx).toBeLessThan(usersIdx)
162
+ })
163
+
164
+ it('keeps standard Content/Settings entries after collections', () => {
165
+ const items = buildNavItems({
166
+ collections: {
167
+ blog: { slug: 'blog', labels: { singular: 'Blog Post', plural: 'Blog' }, type: 'post' },
168
+ },
169
+ })
170
+ const paths = items.map((i) => i.path)
171
+ expect(paths).toContain('/media')
172
+ expect(paths).toContain('/forms')
173
+ expect(paths).toContain('/seo')
174
+ expect(paths).toContain('/script-tags')
175
+ expect(paths).toContain('/users')
176
+ expect(paths).toContain('/api-keys')
177
+ expect(paths).toContain('/settings')
178
+ })
179
+ })