@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,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
+ }
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
4
- import { Search, Bell, User, ChevronDown, Menu, Sun, Moon } from 'lucide-react'
3
+ import { useState, type ReactNode } from 'react'
4
+ import { Search, User, ChevronDown, Menu, Sun, Moon } from 'lucide-react'
5
5
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
6
6
  import { CommandPalette } from '../components/CommandPalette.js'
7
+ import { NotificationBell } from '../components/NotificationBell.js'
7
8
  import { useTheme } from '../components/ThemeProvider.js'
8
9
  import { LocaleSwitcher } from '../components/LocaleSwitcher.js'
9
10
 
@@ -11,9 +12,21 @@ export interface HeaderProps {
11
12
  onToggleSidebar: () => void
12
13
  session?: any
13
14
  onNavigate: (path: string) => void
15
+ /**
16
+ * Page-specific actions rendered on the far right of the top bar
17
+ * (after the search input + utility icons). Used to surface
18
+ * dashboard-level primary actions like "+ New Post" / "View Site"
19
+ * without forcing every view to render its own header.
20
+ *
21
+ * The Layout decides what to pass here based on `currentPath`.
22
+ * Keeping the slot on Header rather than per-view means the
23
+ * actions row stays consistent across navigation and never reflows
24
+ * on view-change.
25
+ */
26
+ pageActions?: ReactNode
14
27
  }
15
28
 
16
- export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
29
+ export function Header({ onToggleSidebar, session, onNavigate, pageActions }: HeaderProps) {
17
30
  const [showCommandPalette, setShowCommandPalette] = useState(false)
18
31
  const [searchQuery, setSearchQuery] = useState('')
19
32
  const { resolvedTheme, setTheme } = useTheme()
@@ -22,9 +35,14 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
22
35
  setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
23
36
  }
24
37
 
38
+ const initial =
39
+ session?.user?.name?.charAt(0)?.toUpperCase() ??
40
+ session?.user?.email?.charAt(0)?.toUpperCase() ??
41
+ 'A'
42
+
25
43
  return (
26
44
  <>
27
- <header className="border-border bg-background flex h-14 items-center justify-between gap-4 border-b px-4">
45
+ <header className="border-border bg-background flex h-14 items-center gap-3 border-b px-4">
28
46
  <button
29
47
  onClick={onToggleSidebar}
30
48
  className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
@@ -33,39 +51,50 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
33
51
  <Menu className="text-foreground h-5 w-5" strokeWidth={2} />
34
52
  </button>
35
53
 
36
- <div className="flex items-center md:hidden">
37
- <span className="text-foreground text-lg font-semibold">Actuate</span>
54
+ {/* Search: takes available width on desktop so it visually anchors
55
+ the top bar like the reference design. */}
56
+ <div className="relative hidden flex-1 md:block">
57
+ <Search
58
+ className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
59
+ aria-hidden
60
+ />
61
+ <input
62
+ type="text"
63
+ placeholder="Search..."
64
+ aria-label="Search content"
65
+ value={searchQuery}
66
+ onChange={(e) => setSearchQuery(e.target.value)}
67
+ onFocus={() => setShowCommandPalette(true)}
68
+ onKeyDown={(e) => {
69
+ if (e.key === 'Escape') (e.target as HTMLInputElement).blur()
70
+ }}
71
+ className="border-border bg-input-background text-foreground placeholder:text-muted-foreground focus:ring-ring h-9 w-full max-w-md rounded-lg border py-1.5 pr-14 pl-9 text-sm focus:border-transparent focus:ring-2 focus:outline-none"
72
+ />
73
+ {/* Keyboard hint pill. Hidden on narrow widths to avoid overlap. */}
74
+ <span
75
+ className="border-border bg-background text-muted-foreground pointer-events-none absolute top-1/2 right-2 hidden -translate-y-1/2 rounded border px-1.5 py-0.5 font-mono text-[10px] sm:inline-flex"
76
+ aria-hidden
77
+ >
78
+ ⌘K
79
+ </span>
38
80
  </div>
39
81
 
40
- <div className="flex-1" />
41
-
42
- <div className="flex items-center gap-2 sm:gap-3">
43
- <div className="relative hidden md:block">
44
- <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
45
- <input
46
- type="text"
47
- placeholder="Search... (⌘K)"
48
- value={searchQuery}
49
- onChange={(e) => setSearchQuery(e.target.value)}
50
- onFocus={() => setShowCommandPalette(true)}
51
- className="border-border bg-input-background text-foreground focus:ring-ring w-64 rounded-lg border py-1.5 pr-3 pl-9 text-sm focus:border-transparent focus:ring-2 focus:outline-none"
52
- />
53
- </div>
54
-
55
- <button
56
- onClick={() => setShowCommandPalette(true)}
57
- className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
58
- aria-label="Search"
59
- >
60
- <Search className="text-muted-foreground h-5 w-5" />
61
- </button>
82
+ {/* Mobile search trigger (icon-only). */}
83
+ <button
84
+ onClick={() => setShowCommandPalette(true)}
85
+ className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
86
+ aria-label="Search"
87
+ >
88
+ <Search className="text-muted-foreground h-5 w-5" />
89
+ </button>
62
90
 
91
+ <div className="ml-auto flex items-center gap-1.5">
63
92
  <LocaleSwitcher />
64
93
 
65
94
  <button
66
95
  onClick={toggleTheme}
67
96
  className="hover:bg-accent rounded-lg p-2 transition-colors"
68
- aria-label="Toggle theme"
97
+ aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
69
98
  >
70
99
  {resolvedTheme === 'dark' ? (
71
100
  <Sun className="text-muted-foreground h-5 w-5" />
@@ -74,23 +103,24 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
74
103
  )}
75
104
  </button>
76
105
 
77
- <button
78
- className="hover:bg-accent relative rounded-lg p-2 transition-colors"
79
- aria-label="Notifications"
80
- >
81
- <Bell className="text-muted-foreground h-5 w-5" />
82
- <span className="bg-destructive absolute top-1.5 right-1.5 h-2 w-2 rounded-full" />
83
- </button>
106
+ {/* Real notification bell — replaces the previous inert decoration.
107
+ Renders its own dropdown panel, SSE-backed unread counter, and
108
+ click-to-mark-read flow. */}
109
+ <NotificationBell />
84
110
 
85
111
  <DropdownMenu.Root>
86
112
  <DropdownMenu.Trigger asChild>
87
- <button className="hover:bg-accent flex items-center gap-2 rounded-lg p-1.5 transition-colors">
113
+ <button
114
+ className="hover:bg-accent flex items-center gap-1.5 rounded-lg p-1 transition-colors"
115
+ aria-label="Account menu"
116
+ >
88
117
  <div className="flex h-8 w-8 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-purple-600">
89
- <span className="text-sm font-medium text-white">
90
- {session?.user?.name?.charAt(0)?.toUpperCase() ?? 'A'}
91
- </span>
118
+ <span className="text-sm font-medium text-white">{initial}</span>
92
119
  </div>
93
- <ChevronDown className="text-muted-foreground hidden h-4 w-4 sm:block" />
120
+ <ChevronDown
121
+ className="text-muted-foreground hidden h-4 w-4 sm:block"
122
+ aria-hidden
123
+ />
94
124
  </button>
95
125
  </DropdownMenu.Trigger>
96
126
 
@@ -106,7 +136,10 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
106
136
  {session?.user?.email ?? 'admin@example.com'}
107
137
  </p>
108
138
  </div>
109
- <DropdownMenu.Item className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none">
139
+ <DropdownMenu.Item
140
+ className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none"
141
+ onSelect={() => onNavigate('/profile')}
142
+ >
110
143
  <User className="h-4 w-4" />
111
144
  Profile
112
145
  </DropdownMenu.Item>
@@ -117,12 +150,26 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
117
150
  Settings
118
151
  </DropdownMenu.Item>
119
152
  <DropdownMenu.Separator className="bg-border my-1 h-px" />
120
- <DropdownMenu.Item className="text-destructive hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none">
153
+ <DropdownMenu.Item
154
+ className="text-destructive hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none"
155
+ onSelect={() => onNavigate('/logout')}
156
+ >
121
157
  Logout
122
158
  </DropdownMenu.Item>
123
159
  </DropdownMenu.Content>
124
160
  </DropdownMenu.Portal>
125
161
  </DropdownMenu.Root>
162
+
163
+ {/* Page actions slot — sits to the right of the utility icons so
164
+ the primary CTA is the rightmost element, mirroring the
165
+ reference. Hidden on mobile because narrow viewports lack the
166
+ room; pages can re-surface the same action in their hero
167
+ there. */}
168
+ {pageActions && (
169
+ <div className="border-border ml-2 hidden items-center gap-2 border-l pl-3 md:flex">
170
+ {pageActions}
171
+ </div>
172
+ )}
126
173
  </div>
127
174
  </header>
128
175
 
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useState, useEffect, type ReactNode } from 'react'
3
+ import { useMemo, useState, useEffect, type ReactNode } from 'react'
4
+ import { ExternalLink, Plus } from 'lucide-react'
4
5
  import { Sidebar } from './Sidebar.js'
5
6
  import { Header } from './Header.js'
6
7
  import { Breadcrumbs } from '../components/Breadcrumbs.js'
@@ -37,6 +38,11 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
37
38
  setMobileSidebarOpen(false)
38
39
  }, [currentPath])
39
40
 
41
+ const pageActions = useMemo(
42
+ () => computePageActions({ currentPath, config, onNavigate }),
43
+ [currentPath, config, onNavigate],
44
+ )
45
+
40
46
  return (
41
47
  <>
42
48
  <Toaster position="bottom-right" />
@@ -57,6 +63,7 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
57
63
  onToggleSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
58
64
  session={session}
59
65
  onNavigate={onNavigate}
66
+ pageActions={pageActions}
60
67
  />
61
68
  }
62
69
  breadcrumbs={<Breadcrumbs currentPath={currentPath} onNavigate={onNavigate} />}
@@ -66,3 +73,161 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
66
73
  </>
67
74
  )
68
75
  }
76
+
77
+ // ─── Page actions ────────────────────────────────────────────────────────────
78
+
79
+ interface PageActionsContext {
80
+ currentPath: string
81
+ config: any
82
+ onNavigate: (path: string) => void
83
+ }
84
+
85
+ /**
86
+ * Per-route primary actions that render on the right of the top bar.
87
+ *
88
+ * Centralised here so the top bar's actions row stays consistent
89
+ * across view changes — no flicker on navigation, no per-view header
90
+ * reimplementation. Views that need view-local actions (e.g. a
91
+ * "Save draft" button on the post editor) continue to render them
92
+ * inside their own hero; this slot is for the highest-level
93
+ * primary/secondary pair that mirrors the reference dashboard.
94
+ *
95
+ * Resolution rules:
96
+ * - Dashboard / home → "+ New Post" + "View Site"
97
+ * - List views → just "+ New <Type>"
98
+ * - Editor views → no top-bar action (editor renders its own toolbar)
99
+ */
100
+ function computePageActions({
101
+ currentPath,
102
+ config,
103
+ onNavigate,
104
+ }: PageActionsContext): ReactNode | null {
105
+ const collections = collectionList(config)
106
+ const postCollections = collections.filter((c) => c.type === 'post')
107
+ const newPostPath = resolveNewPostPath(postCollections)
108
+ const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
109
+
110
+ const isDashboard = currentPath === '/' || currentPath === '/dashboard'
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.
115
+ if (isDashboard) {
116
+ return (
117
+ <>
118
+ <button
119
+ type="button"
120
+ onClick={() => onNavigate(newPostPath)}
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"
122
+ >
123
+ <Plus className="h-3.5 w-3.5" aria-hidden />
124
+ New Post
125
+ </button>
126
+ {siteUrl ? (
127
+ <a
128
+ href={siteUrl}
129
+ target="_blank"
130
+ rel="noopener noreferrer"
131
+ className="border-border bg-background text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
132
+ >
133
+ <ExternalLink className="h-3.5 w-3.5" aria-hidden />
134
+ View Site
135
+ </a>
136
+ ) : null}
137
+ </>
138
+ )
139
+ }
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
+
187
+ // List views — surface "+ New X" so authors can keep creating without
188
+ // hunting for a per-list button. Skip on editor views; those have their
189
+ // own toolbar.
190
+ for (const c of collections) {
191
+ if (currentPath === `/${c.slug}`) {
192
+ return (
193
+ <button
194
+ type="button"
195
+ onClick={() => onNavigate(`/${c.slug}/new`)}
196
+ 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"
197
+ >
198
+ <Plus className="h-3.5 w-3.5" aria-hidden />
199
+ New {c.labels?.singular ?? c.slug}
200
+ </button>
201
+ )
202
+ }
203
+ }
204
+
205
+ return null
206
+ }
207
+
208
+ interface CollectionMeta {
209
+ slug: string
210
+ type?: string
211
+ labels?: { singular?: string; plural?: string }
212
+ admin?: { hidden?: boolean }
213
+ }
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
+
228
+ function collectionList(config: any): CollectionMeta[] {
229
+ if (!config?.collections) return []
230
+ const raw = config.collections
231
+ const list: any[] = Array.isArray(raw) ? raw : Object.values(raw)
232
+ return list.filter((c) => !c?.admin?.hidden)
233
+ }