@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,104 @@
1
+ 'use client'
2
+
3
+ import { paletteFor } from '../posts/badges.js'
4
+
5
+ /**
6
+ * Tag UI for the Pages area. Two variants:
7
+ * - PageTagChip — interactive filter chip (filter bar). Toggles a tag
8
+ * on/off; announces pressed state for screen readers.
9
+ * - PageTagPill — static inline pill (Tags column). Compact, with a
10
+ * colored dot so tags don't rely on color alone.
11
+ *
12
+ * Both resolve color through the shared `paletteFor` palette so they
13
+ * stay consistent with Post Type badges and work in light/dark mode.
14
+ */
15
+
16
+ const DOT_COLOR: Record<string, string> = {
17
+ purple: 'bg-violet-500',
18
+ green: 'bg-emerald-500',
19
+ orange: 'bg-amber-500',
20
+ red: 'bg-red-500',
21
+ blue: 'bg-blue-500',
22
+ pink: 'bg-pink-500',
23
+ gray: 'bg-muted-foreground',
24
+ }
25
+
26
+ export function tagDotColor(color: string | null | undefined): string {
27
+ if (!color) return DOT_COLOR.gray!
28
+ return DOT_COLOR[color] ?? DOT_COLOR.gray!
29
+ }
30
+
31
+ export function PageTagChip({
32
+ name,
33
+ color,
34
+ count,
35
+ selected,
36
+ onToggle,
37
+ }: {
38
+ name: string
39
+ color: string | null
40
+ count?: number
41
+ selected: boolean
42
+ onToggle: () => void
43
+ }) {
44
+ const p = paletteFor(color)
45
+ return (
46
+ <button
47
+ type="button"
48
+ onClick={onToggle}
49
+ aria-pressed={selected}
50
+ className={`focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 ${
51
+ selected
52
+ ? `${p.bg} ${p.text} border-transparent ring-1 ${p.ring}`
53
+ : 'border-border bg-background text-foreground hover:bg-accent'
54
+ }`}
55
+ >
56
+ <span aria-hidden className={`h-2 w-2 rounded-full ${tagDotColor(color)}`} />
57
+ {name}
58
+ {typeof count === 'number' && (
59
+ <span className="text-muted-foreground tabular-nums">{count}</span>
60
+ )}
61
+ </button>
62
+ )
63
+ }
64
+
65
+ export function PageTagPill({ name, color }: { name: string; color: string | null }) {
66
+ const p = paletteFor(color)
67
+ return (
68
+ <span
69
+ className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`}
70
+ >
71
+ <span aria-hidden className={`h-1.5 w-1.5 rounded-full ${tagDotColor(color)}`} />
72
+ {name}
73
+ </span>
74
+ )
75
+ }
76
+
77
+ export function PageTagsCell({
78
+ tagSlugs,
79
+ primaryTagId,
80
+ resolve,
81
+ }: {
82
+ tagSlugs: string[]
83
+ primaryTagId: string | null
84
+ resolve: (slug: string) => { name: string; color: string | null } | undefined
85
+ }) {
86
+ if (tagSlugs.length === 0) {
87
+ return <span className="text-muted-foreground text-xs">—</span>
88
+ }
89
+ // Show the primary tag first, then the rest, so grouping context is
90
+ // visible at a glance.
91
+ const ordered = [...tagSlugs].sort((a, b) => {
92
+ if (a === primaryTagId) return -1
93
+ if (b === primaryTagId) return 1
94
+ return 0
95
+ })
96
+ return (
97
+ <div className="flex flex-wrap items-center gap-1">
98
+ {ordered.map((slug) => {
99
+ const tag = resolve(slug)
100
+ return <PageTagPill key={slug} name={tag?.name ?? slug} color={tag?.color ?? null} />
101
+ })}
102
+ </div>
103
+ )
104
+ }
@@ -0,0 +1,170 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+ import type { PostStatus } from '../../lib/posts-service.js'
5
+
6
+ /**
7
+ * Shared status / type / SEO indicators for the Posts admin area.
8
+ *
9
+ * Colors are restricted to the theme tokens already used elsewhere in
10
+ * the admin (`success`, `warning`, `destructive`, `primary`, `muted`)
11
+ * plus a small named-color palette (`purple`, `green`, `orange`, ...)
12
+ * that maps to fixed Tailwind utilities. This keeps the badges working
13
+ * in both light and dark mode and avoids leaking `bg-[#...]` arbitrary
14
+ * values into components.
15
+ */
16
+
17
+ // ─── Named color palette ─────────────────────────────────────────────
18
+
19
+ const PALETTE: Record<string, { bg: string; text: string; ring: string; border: string }> = {
20
+ purple: {
21
+ bg: 'bg-violet-50 dark:bg-violet-950/40',
22
+ text: 'text-violet-700 dark:text-violet-300',
23
+ ring: 'ring-violet-200 dark:ring-violet-900',
24
+ border: 'border-violet-500',
25
+ },
26
+ green: {
27
+ bg: 'bg-emerald-50 dark:bg-emerald-950/40',
28
+ text: 'text-emerald-700 dark:text-emerald-300',
29
+ ring: 'ring-emerald-200 dark:ring-emerald-900',
30
+ border: 'border-emerald-500',
31
+ },
32
+ orange: {
33
+ bg: 'bg-amber-50 dark:bg-amber-950/40',
34
+ text: 'text-amber-700 dark:text-amber-300',
35
+ ring: 'ring-amber-200 dark:ring-amber-900',
36
+ border: 'border-amber-500',
37
+ },
38
+ red: {
39
+ bg: 'bg-red-50 dark:bg-red-950/40',
40
+ text: 'text-red-700 dark:text-red-300',
41
+ ring: 'ring-red-200 dark:ring-red-900',
42
+ border: 'border-red-500',
43
+ },
44
+ blue: {
45
+ bg: 'bg-blue-50 dark:bg-blue-950/40',
46
+ text: 'text-blue-700 dark:text-blue-300',
47
+ ring: 'ring-blue-200 dark:ring-blue-900',
48
+ border: 'border-blue-500',
49
+ },
50
+ pink: {
51
+ bg: 'bg-pink-50 dark:bg-pink-950/40',
52
+ text: 'text-pink-700 dark:text-pink-300',
53
+ ring: 'ring-pink-200 dark:ring-pink-900',
54
+ border: 'border-pink-500',
55
+ },
56
+ gray: {
57
+ bg: 'bg-muted',
58
+ text: 'text-muted-foreground',
59
+ ring: 'ring-border',
60
+ border: 'border-border',
61
+ },
62
+ }
63
+
64
+ export function paletteFor(color: string | null | undefined) {
65
+ if (!color) return PALETTE.gray!
66
+ return PALETTE[color] ?? PALETTE.gray!
67
+ }
68
+
69
+ // ─── PostStatusBadge ─────────────────────────────────────────────────
70
+
71
+ const STATUS_LABEL: Record<PostStatus, string> = {
72
+ PUBLISHED: 'Published',
73
+ DRAFT: 'Draft',
74
+ SCHEDULED: 'Scheduled',
75
+ ARCHIVED: 'Archived',
76
+ }
77
+
78
+ const STATUS_PALETTE: Record<PostStatus, string> = {
79
+ PUBLISHED: 'green',
80
+ DRAFT: 'gray',
81
+ SCHEDULED: 'orange',
82
+ ARCHIVED: 'gray',
83
+ }
84
+
85
+ const STATUS_GLYPH: Record<PostStatus, string> = {
86
+ PUBLISHED: '●',
87
+ DRAFT: '○',
88
+ SCHEDULED: '◐',
89
+ ARCHIVED: '◌',
90
+ }
91
+
92
+ export function PostStatusBadge({ status }: { status: PostStatus }) {
93
+ const p = paletteFor(STATUS_PALETTE[status])
94
+ return (
95
+ <span
96
+ className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`}
97
+ aria-label={`Status: ${STATUS_LABEL[status]}`}
98
+ >
99
+ <span aria-hidden className="text-[10px] leading-none">
100
+ {STATUS_GLYPH[status]}
101
+ </span>
102
+ {STATUS_LABEL[status]}
103
+ </span>
104
+ )
105
+ }
106
+
107
+ // ─── PostTypeBadge ───────────────────────────────────────────────────
108
+
109
+ export function PostTypeBadge({
110
+ label,
111
+ color,
112
+ onClick,
113
+ }: {
114
+ label: string
115
+ color: string | null
116
+ onClick?: () => void
117
+ }): ReactNode {
118
+ const p = paletteFor(color)
119
+ const className = `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`
120
+ if (onClick) {
121
+ return (
122
+ <button type="button" onClick={onClick} className={`${className} hover:opacity-80`}>
123
+ {label}
124
+ </button>
125
+ )
126
+ }
127
+ return <span className={className}>{label}</span>
128
+ }
129
+
130
+ // ─── SeoScoreIndicator ───────────────────────────────────────────────
131
+
132
+ /**
133
+ * Returns the palette key for an SEO score so callers can match the
134
+ * colored dot. 80+ green, 60–79 orange, 1–59 red, missing = gray.
135
+ */
136
+ export function seoScorePalette(score: number | null): string {
137
+ if (score === null || Number.isNaN(score)) return 'gray'
138
+ if (score >= 80) return 'green'
139
+ if (score >= 60) return 'orange'
140
+ if (score >= 1) return 'red'
141
+ return 'gray'
142
+ }
143
+
144
+ export function SeoScoreIndicator({ score }: { score: number | null }) {
145
+ const colorKey = seoScorePalette(score)
146
+ const p = paletteFor(colorKey)
147
+ if (score === null) {
148
+ return (
149
+ <span className="text-muted-foreground inline-flex items-center gap-1.5 text-xs">
150
+ <span aria-hidden className={`h-2 w-2 rounded-full ${p.bg.split(' ')[0]}`} />
151
+ <span className="tabular-nums">—</span>
152
+ </span>
153
+ )
154
+ }
155
+ return (
156
+ <span className="text-foreground inline-flex items-center gap-1.5 text-xs">
157
+ <span
158
+ aria-hidden
159
+ className={`h-2 w-2 rounded-full ${
160
+ colorKey === 'green'
161
+ ? 'bg-emerald-500'
162
+ : colorKey === 'orange'
163
+ ? 'bg-amber-500'
164
+ : 'bg-red-500'
165
+ }`}
166
+ />
167
+ <span className="tabular-nums">{score}</span>
168
+ </span>
169
+ )
170
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ FileText,
3
+ Briefcase,
4
+ Newspaper,
5
+ BookOpen,
6
+ Folder,
7
+ Layers,
8
+ Image as ImageIcon,
9
+ Calendar,
10
+ Users,
11
+ HelpCircle,
12
+ type LucideIcon,
13
+ } from 'lucide-react'
14
+
15
+ /**
16
+ * Icon registry for Post Types. Keyed by the string value stored in a
17
+ * collection's `admin.icon`. Rendering surfaces (Post Types cards, the
18
+ * New Post picker, the Quick Post Type gallery) all resolve through
19
+ * `resolvePostTypeIcon` so a single map keeps them in sync — adding an
20
+ * icon here makes it available everywhere at once.
21
+ */
22
+ export const POST_TYPE_ICONS: Record<string, LucideIcon> = {
23
+ 'file-text': FileText,
24
+ briefcase: Briefcase,
25
+ newspaper: Newspaper,
26
+ book: BookOpen,
27
+ folder: Folder,
28
+ layers: Layers,
29
+ image: ImageIcon,
30
+ calendar: Calendar,
31
+ users: Users,
32
+ help: HelpCircle,
33
+ }
34
+
35
+ export type PostTypeIconName = keyof typeof POST_TYPE_ICONS
36
+
37
+ export function resolvePostTypeIcon(name: string | null | undefined): LucideIcon {
38
+ if (!name) return FileText
39
+ return POST_TYPE_ICONS[name.toLowerCase()] ?? FileText
40
+ }
@@ -102,29 +102,26 @@ function computePageActions({
102
102
  config,
103
103
  onNavigate,
104
104
  }: PageActionsContext): ReactNode | null {
105
- // Resolve the "primary" content collection (used for both the dashboard
106
- // and any explicit /posts route). Prefer a real `posts` or `post`-type
107
- // collection over the hard-coded slug so admin-managed CMS configs work.
108
105
  const collections = collectionList(config)
109
- const postsCol =
110
- collections.find((c) => c.slug === 'posts' || c.type === 'post') ??
111
- collections.find((c) => c.type === 'post')
112
- const postsSlug = postsCol?.slug ?? 'posts'
113
- const postsLabel = postsCol?.labels?.singular ?? 'Post'
106
+ const postCollections = collections.filter((c) => c.type === 'post')
107
+ const newPostPath = resolveNewPostPath(postCollections)
114
108
  const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
115
109
 
116
110
  const isDashboard = currentPath === '/' || currentPath === '/dashboard'
117
111
 
112
+ // Dashboard: keep the existing "+ New Post" / "View Site" pair. With
113
+ // a single Post Type we route straight to its editor; with several we
114
+ // route to the picker so the user can choose the type first.
118
115
  if (isDashboard) {
119
116
  return (
120
117
  <>
121
118
  <button
122
119
  type="button"
123
- onClick={() => onNavigate(`/${postsSlug}/new`)}
120
+ onClick={() => onNavigate(newPostPath)}
124
121
  className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
125
122
  >
126
123
  <Plus className="h-3.5 w-3.5" aria-hidden />
127
- New {postsLabel}
124
+ New Post
128
125
  </button>
129
126
  {siteUrl ? (
130
127
  <a
@@ -141,6 +138,52 @@ function computePageActions({
141
138
  )
142
139
  }
143
140
 
141
+ // Posts area — "+ New Post" + "Manage types" on the list page;
142
+ // "+ New Post Type" on the types page. The buttons live in the top
143
+ // bar so the page header stays clean. See the dashboard rule for
144
+ // why these come from a single config-aware helper.
145
+ if (currentPath === '/posts') {
146
+ return (
147
+ <button
148
+ type="button"
149
+ onClick={() => onNavigate(newPostPath)}
150
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
151
+ >
152
+ <Plus className="h-3.5 w-3.5" aria-hidden />
153
+ New Post
154
+ </button>
155
+ )
156
+ }
157
+
158
+ if (currentPath === '/posts/types') {
159
+ return (
160
+ <button
161
+ type="button"
162
+ onClick={() => onNavigate('/posts/types/new')}
163
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
164
+ >
165
+ <Plus className="h-3.5 w-3.5" aria-hidden />
166
+ New Post Type
167
+ </button>
168
+ )
169
+ }
170
+
171
+ // Pages list — "+ New page" routes to the page creation flow. Handled
172
+ // explicitly (not via the collection loop) so the label/route stay
173
+ // exact even though `pages` is a reserved route.
174
+ if (currentPath === '/pages') {
175
+ return (
176
+ <button
177
+ type="button"
178
+ onClick={() => onNavigate('/pages/new')}
179
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
180
+ >
181
+ <Plus className="h-3.5 w-3.5" aria-hidden />
182
+ New page
183
+ </button>
184
+ )
185
+ }
186
+
144
187
  // List views — surface "+ New X" so authors can keep creating without
145
188
  // hunting for a per-list button. Skip on editor views; those have their
146
189
  // own toolbar.
@@ -169,6 +212,19 @@ interface CollectionMeta {
169
212
  admin?: { hidden?: boolean }
170
213
  }
171
214
 
215
+ /**
216
+ * Where "+ New Post" should land, given the configured post-type
217
+ * collections:
218
+ * - exactly one → straight to that type's editor (`/<slug>/new`)
219
+ * - more than one → the type picker (`/posts/new`)
220
+ * - none → the Post Type setup instructions
221
+ */
222
+ function resolveNewPostPath(postCollections: CollectionMeta[]): string {
223
+ if (postCollections.length === 1) return `/${postCollections[0]!.slug}/new`
224
+ if (postCollections.length > 1) return '/posts/new'
225
+ return '/posts/types/new'
226
+ }
227
+
172
228
  function collectionList(config: any): CollectionMeta[] {
173
229
  if (!config?.collections) return []
174
230
  const raw = config.collections
@@ -19,9 +19,12 @@ import {
19
19
  PanelTop,
20
20
  PanelBottom,
21
21
  Layers,
22
+ LayoutGrid,
22
23
  Code2,
23
24
  Library,
24
25
  KeyRound,
26
+ ListTree,
27
+ Menu as MenuIcon,
25
28
  PanelLeftClose,
26
29
  PanelLeftOpen,
27
30
  } from 'lucide-react'
@@ -61,6 +64,9 @@ const ICON_MAP: Record<string, LucideIcon> = {
61
64
  book: BookOpen,
62
65
  help: HelpCircle,
63
66
  newspaper: Newspaper,
67
+ navigation: ListTree,
68
+ menu: MenuIcon,
69
+ 'list-tree': ListTree,
64
70
  PanelTop: PanelTop,
65
71
  PanelBottom: PanelBottom,
66
72
  Layers: Layers,
@@ -106,24 +112,45 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
106
112
  }
107
113
 
108
114
  /**
109
- * Nav tree for the default (config-less) install. Mirrors the reference
110
- * dashboard exactly: Dashboard / Posts / Pages (with Saved Sections +
111
- * Templates as nested children), then a CONTENT group (Media / Forms /
112
- * SEO / Script Tags), then a SETTINGS group (Users / API Keys /
113
- * Settings). The group ordering is meaningful — the renderer emits a
114
- * section header the first time it sees a new group.
115
+ * Children of the Posts parent. Same shape regardless of how many
116
+ * post-type collections are registered Post Types are accessed via
117
+ * the `/posts/types` cards page, not as individual sidebar entries.
118
+ */
119
+ const POSTS_CHILDREN: NavItem[] = [
120
+ { path: '/posts', label: 'All Posts', icon: FileText },
121
+ { path: '/posts/types', label: 'Post Types', icon: Layers },
122
+ ]
123
+
124
+ /**
125
+ * Children of the Templates parent. Templates is its own top-level item
126
+ * (page building tools) — Saved Sections are reusable content blocks and
127
+ * Widgets are embeddable components. These are deliberately NOT children
128
+ * of Pages: Pages is content, Templates/Sections/Widgets are structure.
129
+ */
130
+ const TEMPLATES_CHILDREN: NavItem[] = [
131
+ { path: '/saved-sections', label: 'Saved Sections', icon: Library },
132
+ { path: '/widgets', label: 'Widgets', icon: LayoutGrid },
133
+ ]
134
+
135
+ /**
136
+ * Nav tree for the default (config-less) install. Dashboard sits on its
137
+ * own at the top, then a CONTENT group holding Pages (no children) above
138
+ * Posts (with All Posts + Post Types) and Templates (with Saved Sections
139
+ * + Widgets), followed by Media / Forms / SEO / Script Tags, then a
140
+ * SETTINGS group (Users / API Keys / Settings). The group ordering is
141
+ * meaningful — the renderer emits a section header the first time it
142
+ * sees a new group, so "CONTENT" renders once, above Pages.
115
143
  */
116
144
  const defaultNavItems: NavItem[] = [
117
145
  { path: '/', label: 'Dashboard', icon: LayoutDashboard },
118
- { path: '/posts', label: 'Posts', icon: FileText },
146
+ { path: '/pages', label: 'Pages', icon: File, group: 'Content' },
147
+ { path: '/posts', label: 'Posts', icon: FileText, group: 'Content', children: POSTS_CHILDREN },
119
148
  {
120
- path: '/pages',
121
- label: 'Pages',
122
- icon: File,
123
- children: [
124
- { path: '/saved-sections', label: 'Saved Sections', icon: Library },
125
- { path: '/page-templates', label: 'Templates', icon: Layers },
126
- ],
149
+ path: '/page-templates',
150
+ label: 'Templates',
151
+ icon: Layers,
152
+ group: 'Content',
153
+ children: TEMPLATES_CHILDREN,
127
154
  },
128
155
  { path: '/media', label: 'Media', icon: Image, group: 'Content' },
129
156
  { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
@@ -207,6 +234,15 @@ interface NavRenderContext {
207
234
 
208
235
  function isPathActive(currentPath: string, itemPath: string): boolean {
209
236
  if (itemPath === '/') return currentPath === '/'
237
+
238
+ // Special case: /posts is a prefix of /posts/types — without this
239
+ // guard, "All Posts" (/posts) would stay highlighted while the user
240
+ // is on /posts/types or /posts/types/new. Treat /posts as exact-only
241
+ // when a sibling /posts/* route exists.
242
+ if (itemPath === '/posts') {
243
+ return currentPath === '/posts' || currentPath === '/posts/new'
244
+ }
245
+
210
246
  // Use boundary match so /pages doesn't match /pages-something. Exact, or
211
247
  // followed by a `/` separator.
212
248
  return currentPath === itemPath || currentPath.startsWith(itemPath + '/')
@@ -236,10 +272,16 @@ function renderNavTree(items: NavItem[], ctx: NavRenderContext): ReactNode {
236
272
  lastGroup = undefined
237
273
  }
238
274
 
239
- nodes.push(<NavLink key={item.path} item={item} ctx={ctx} />)
275
+ // Keys are namespaced because a parent and its first child can share
276
+ // the same path (e.g. the Posts parent and its "All Posts" child are
277
+ // both `/posts`). Using the raw path as the key collided and made
278
+ // React warn about — and risk dropping — duplicate siblings.
279
+ nodes.push(<NavLink key={`item:${item.path}`} item={item} ctx={ctx} />)
240
280
  if (item.children && item.children.length > 0 && !ctx.collapsed) {
241
281
  for (const child of item.children) {
242
- nodes.push(<NavLink key={child.path} item={child} ctx={ctx} nested />)
282
+ nodes.push(
283
+ <NavLink key={`child:${item.path}:${child.path}`} item={child} ctx={ctx} nested />,
284
+ )
243
285
  }
244
286
  }
245
287
  }
@@ -315,24 +357,32 @@ export interface NavItem {
315
357
  * destination (Media, Forms, SEO, Script Tags, Users, API Keys,
316
358
  * Settings) is anchored under the right Content / Settings group.
317
359
  */
318
- function buildNavItems(config: any): NavItem[] {
360
+ export function buildNavItems(config: any): NavItem[] {
319
361
  if (!config?.collections) return defaultNavItems
320
362
 
321
363
  const raw = config.collections
322
364
  const collectionsList: any[] = Array.isArray(raw) ? raw : Object.values(raw)
323
365
  const visible = collectionsList.filter((c) => !c.admin?.hidden)
324
366
 
367
+ // Treat `admin.group: 'Posts'` (case-insensitive) as a synonym for
368
+ // `type: 'post'` so consumers can tag arbitrary collections into
369
+ // the Posts area without changing the discriminator they already
370
+ // use elsewhere (sitemaps, schema.org, etc.).
371
+ const isPostsGroup = (c: any) =>
372
+ typeof c.admin?.group === 'string' && c.admin.group.toLowerCase() === 'posts'
325
373
  const pages = visible.filter((c) => c.type === 'page' && !c.admin?.group)
326
- const posts = visible.filter((c) => c.type === 'post' && !c.admin?.group)
374
+ const posts = visible.filter((c) => c.type === 'post' || isPostsGroup(c))
327
375
  const otherUngrouped = visible.filter(
328
376
  (c) => c.type !== 'page' && c.type !== 'post' && !c.admin?.group,
329
377
  )
330
- const grouped = visible.filter((c) => c.admin?.group)
378
+ // Anything explicitly grouped that isn't a posts collection.
379
+ const grouped = visible.filter((c) => c.admin?.group && !isPostsGroup(c) && c.type !== 'post')
331
380
 
332
381
  const items: NavItem[] = [{ path: '/', label: 'Dashboard', icon: LayoutDashboard }]
333
382
 
334
- // Posts-style collections come first (mirrors the reference layout).
335
- for (const collection of posts) {
383
+ // Ungrouped collections that are neither page- nor post-type stay at
384
+ // the top level, directly under Dashboard, before the CONTENT group.
385
+ for (const collection of otherUngrouped) {
336
386
  items.push({
337
387
  label: collection.labels?.plural ?? collection.slug,
338
388
  path: `/${collection.slug}`,
@@ -340,37 +390,59 @@ function buildNavItems(config: any): NavItem[] {
340
390
  })
341
391
  }
342
392
 
343
- // Pages-style collections get the nested Saved Sections + Templates
344
- // tree. We attach the children to the *first* page-type collection
345
- // so we don't end up with three "Saved Sections" links if a project
346
- // configures multiple page collections — admins editing custom
347
- // page-style content still navigate to those secondary surfaces via
348
- // the primary Pages parent.
349
- pages.forEach((collection, idx) => {
350
- const item: NavItem = {
393
+ // CONTENT group. Everything below shares `group: 'Content'`, so the
394
+ // renderer emits the "CONTENT" header once above Pages. Order:
395
+ // Pages Posts static content destinations (Media / Forms / SEO /
396
+ // Script Tags).
397
+
398
+ // Pages-style collections render as plain Content entries (no nested
399
+ // children). Saved Sections / Widgets now live under the dedicated
400
+ // Templates parent below, not under Pages.
401
+ pages.forEach((collection) => {
402
+ items.push({
351
403
  label: collection.labels?.plural ?? collection.slug,
352
404
  path: `/${collection.slug}`,
353
405
  icon: resolveIcon(collection),
354
- }
355
- if (idx === 0) {
356
- item.children = [
357
- { path: '/saved-sections', label: 'Saved Sections', icon: Library },
358
- { path: '/page-templates', label: 'Templates', icon: Layers },
359
- ]
360
- }
361
- items.push(item)
406
+ group: 'Content',
407
+ })
362
408
  })
363
409
 
364
- for (const collection of otherUngrouped) {
410
+ // Posts area — collapses every post-type collection under a single
411
+ // "Posts" parent. The aggregated table at /posts and the type cards
412
+ // at /posts/types are the entry points; individual post-type
413
+ // collections are not enumerated in the sidebar to avoid the
414
+ // duplicate-nav problem when a project has many types.
415
+ if (posts.length > 0) {
365
416
  items.push({
366
- label: collection.labels?.plural ?? collection.slug,
367
- path: `/${collection.slug}`,
368
- icon: resolveIcon(collection),
417
+ path: '/posts',
418
+ label: 'Posts',
419
+ icon: FileText,
420
+ group: 'Content',
421
+ children: POSTS_CHILDREN,
369
422
  })
370
423
  }
371
424
 
372
- // Config-supplied groups land before our standard Content/Settings
373
- // groups so admin-defined sections take precedence.
425
+ // Templates its own parent holding Saved Sections + Widgets (page
426
+ // building structure, distinct from Pages content).
427
+ items.push({
428
+ path: '/page-templates',
429
+ label: 'Templates',
430
+ icon: Layers,
431
+ group: 'Content',
432
+ children: TEMPLATES_CHILDREN,
433
+ })
434
+
435
+ // Standard Content destinations (always present — they map to CMS
436
+ // features, not collections).
437
+ items.push(
438
+ { path: '/media', label: 'Media', icon: Image, group: 'Content' },
439
+ { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
440
+ { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
441
+ { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
442
+ )
443
+
444
+ // Config-supplied custom groups land after the standard Content block
445
+ // and before Settings. Each renders under its own header.
374
446
  const groupBuckets = new Map<string, NavItem[]>()
375
447
  for (const col of grouped) {
376
448
  const group = col.admin.group as string
@@ -385,14 +457,8 @@ function buildNavItems(config: any): NavItem[] {
385
457
  }
386
458
  for (const bucket of groupBuckets.values()) items.push(...bucket)
387
459
 
388
- // Standard Content + Settings destinations. These are always
389
- // present, regardless of which collections the admin configures,
390
- // because they map to CMS features (not collections).
460
+ // Settings destinations always last.
391
461
  items.push(
392
- { path: '/media', label: 'Media', icon: Image, group: 'Content' },
393
- { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
394
- { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
395
- { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
396
462
  { path: '/users', label: 'Users', icon: Users, group: 'Settings' },
397
463
  { path: '/api-keys', label: 'API Keys', icon: KeyRound, group: 'Settings' },
398
464
  { path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },