@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,147 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { ArrowRight, ArrowLeft } from 'lucide-react'
5
+ import { fetchPostTypes, type PostType } from '../../lib/posts-service.js'
6
+ import { paletteFor } from '../../components/posts/badges.js'
7
+ import { resolvePostTypeIcon } from '../../components/posts/icons.js'
8
+
9
+ /**
10
+ * Type-picker landing page for "+ New Post".
11
+ *
12
+ * When zero or one post type is configured we'd ordinarily auto-route
13
+ * to the per-collection editor, but the spec calls out always showing
14
+ * a chooser when "+ New Post" is clicked. We keep that behavior so
15
+ * the experience is consistent regardless of how many types exist.
16
+ *
17
+ * Picking a card navigates to the section-based Post editor's new-post
18
+ * route (`/posts/<slug>/new`), which seeds the post from that type's
19
+ * template so the author starts from the type's default layout.
20
+ */
21
+
22
+ export function NewPostPicker({ onNavigate }: { onNavigate: (path: string) => void }) {
23
+ const [types, setTypes] = useState<PostType[]>([])
24
+ const [loading, setLoading] = useState(true)
25
+ const [error, setError] = useState<string | null>(null)
26
+
27
+ useEffect(() => {
28
+ let cancelled = false
29
+ fetchPostTypes()
30
+ .then((res) => {
31
+ if (cancelled) return
32
+ // With a single Post Type there's nothing to choose — forward
33
+ // straight to its editor so the picker doesn't flash a
34
+ // one-card screen.
35
+ if (res.length === 1) {
36
+ onNavigate(`/posts/${res[0]!.slug}/new`)
37
+ return
38
+ }
39
+ setTypes(res)
40
+ setLoading(false)
41
+ })
42
+ .catch((err: unknown) => {
43
+ if (cancelled) return
44
+ setError(err instanceof Error ? err.message : 'Failed to load Post Types')
45
+ setLoading(false)
46
+ })
47
+ return () => {
48
+ cancelled = true
49
+ }
50
+ }, [onNavigate])
51
+
52
+ return (
53
+ <div className="mx-auto flex h-full max-w-3xl flex-col gap-6 p-4 sm:p-6">
54
+ <button
55
+ type="button"
56
+ onClick={() => onNavigate('/posts')}
57
+ className="text-muted-foreground hover:text-foreground inline-flex w-fit items-center gap-1.5 text-sm transition-colors"
58
+ >
59
+ <ArrowLeft className="h-3.5 w-3.5" aria-hidden />
60
+ Back to Posts
61
+ </button>
62
+
63
+ <div className="flex flex-col gap-1">
64
+ <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
65
+ Choose a Post Type
66
+ </h1>
67
+ <p className="text-muted-foreground text-sm">
68
+ Pick which content model you want to use for this post. Each type has its own fields, URL
69
+ base, and SEO defaults.
70
+ </p>
71
+ </div>
72
+
73
+ {loading && <Skeleton />}
74
+ {!loading && error && (
75
+ <div className="border-border bg-card rounded-xl border p-8 text-center">
76
+ <div className="text-foreground mb-2 text-sm font-medium">{error}</div>
77
+ </div>
78
+ )}
79
+ {!loading && !error && types.length === 0 && (
80
+ <div className="border-border bg-card rounded-xl border p-8 text-center">
81
+ <div className="text-foreground mb-2 text-sm font-medium">No Post Types configured.</div>
82
+ <p className="text-muted-foreground mb-4 text-sm">
83
+ Add a collection with <code className="font-mono">type: 'post'</code> to your CMS config
84
+ to start creating posts.
85
+ </p>
86
+ <button
87
+ type="button"
88
+ onClick={() => onNavigate('/posts/types/new')}
89
+ className="text-primary text-sm hover:underline"
90
+ >
91
+ View setup instructions →
92
+ </button>
93
+ </div>
94
+ )}
95
+ {!loading && !error && types.length > 0 && (
96
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
97
+ {types.map((t) => {
98
+ const palette = paletteFor(t.accentColor)
99
+ const Icon = resolvePostTypeIcon(t.icon)
100
+ return (
101
+ <button
102
+ key={t.slug}
103
+ type="button"
104
+ onClick={() => onNavigate(`/posts/${t.slug}/new`)}
105
+ className="border-border bg-card focus-visible:ring-ring flex items-center gap-3 rounded-xl border p-4 text-left shadow-sm transition-shadow hover:shadow-md focus:outline-none focus-visible:ring-2"
106
+ >
107
+ <span
108
+ className={`inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${palette.bg} ${palette.text}`}
109
+ >
110
+ <Icon className="h-5 w-5" aria-hidden />
111
+ </span>
112
+ <span className="min-w-0 flex-1">
113
+ <span className="text-foreground block text-sm font-medium">
114
+ {t.singularName}
115
+ </span>
116
+ <span className="text-muted-foreground block truncate text-xs">
117
+ {t.description ?? `New entry in ${t.name}`}
118
+ </span>
119
+ </span>
120
+ <ArrowRight className="text-muted-foreground h-4 w-4 shrink-0" aria-hidden />
121
+ </button>
122
+ )
123
+ })}
124
+ </div>
125
+ )}
126
+ </div>
127
+ )
128
+ }
129
+
130
+ function Skeleton() {
131
+ return (
132
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
133
+ {Array.from({ length: 4 }).map((_, i) => (
134
+ <div
135
+ key={i}
136
+ className="border-border bg-card flex items-center gap-3 rounded-xl border p-4"
137
+ >
138
+ <div className="bg-muted h-10 w-10 animate-pulse rounded-lg" />
139
+ <div className="flex-1">
140
+ <div className="bg-muted mb-2 h-3 w-1/3 animate-pulse rounded" />
141
+ <div className="bg-muted h-2.5 w-2/3 animate-pulse rounded" />
142
+ </div>
143
+ </div>
144
+ ))}
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,344 @@
1
+ 'use client'
2
+
3
+ import { useMemo, useState } from 'react'
4
+ import { ArrowLeft, Check, Copy, Sparkles } from 'lucide-react'
5
+ import { toast } from 'sonner'
6
+ import { resolvePostTypeIcon, type PostTypeIconName } from '../../components/posts/icons.js'
7
+ import { paletteFor } from '../../components/posts/badges.js'
8
+ import { POST_TYPE_PRESETS, type PresetColor, type QuickPreset } from './post-type-presets.js'
9
+
10
+ /**
11
+ * /posts/types/new — Quick Post Type gallery + snippet generator.
12
+ *
13
+ * Two ways to start:
14
+ * 1. Pick a Quick Post Type preset (Case Studies, News, Portfolio,
15
+ * Events, Team) — pre-fills the form and tailored extra fields.
16
+ * 2. Fill the form from scratch.
17
+ *
18
+ * Either way the page emits a paste-ready `actuate.config.ts` snippet.
19
+ *
20
+ * Why this isn't a runtime form-driven CRUD:
21
+ * Collections in ActuateCMS are file-defined in `actuate.config.ts`.
22
+ * Writing to that file at runtime would require either a parallel
23
+ * DB-only post-type model that duplicates the Collection concept, or
24
+ * mutating a source file on disk — which breaks Vercel's read-only
25
+ * serverless filesystem and any immutable deploy model. So we
26
+ * generate the snippet and the developer commits + redeploys. Same
27
+ * model Sanity, Keystone, and Payload use for code-defined schemas.
28
+ */
29
+
30
+ const SLUG_RE = /^[a-z][a-z0-9-]*$/
31
+
32
+ const ICON_OPTIONS: { value: PostTypeIconName; label: string }[] = [
33
+ { value: 'file-text', label: 'Document' },
34
+ { value: 'briefcase', label: 'Briefcase' },
35
+ { value: 'newspaper', label: 'Newspaper' },
36
+ { value: 'book', label: 'Book' },
37
+ { value: 'folder', label: 'Folder' },
38
+ { value: 'layers', label: 'Layers' },
39
+ { value: 'image', label: 'Image' },
40
+ { value: 'calendar', label: 'Calendar' },
41
+ { value: 'users', label: 'People' },
42
+ { value: 'help', label: 'Help' },
43
+ ]
44
+
45
+ interface DraftType {
46
+ slug: string
47
+ singular: string
48
+ plural: string
49
+ color: PresetColor
50
+ icon: PostTypeIconName
51
+ description: string
52
+ /** Extra field lines beyond the shared base, seeded by a preset. */
53
+ extraFields: string[]
54
+ }
55
+
56
+ const BLANK_DRAFT: DraftType = {
57
+ slug: '',
58
+ singular: '',
59
+ plural: '',
60
+ color: 'purple',
61
+ icon: 'file-text',
62
+ description: '',
63
+ extraFields: [],
64
+ }
65
+
66
+ export function NewPostTypeInstructions({ onNavigate }: { onNavigate: (p: string) => void }) {
67
+ const [draft, setDraft] = useState<DraftType>(BLANK_DRAFT)
68
+ const [activePreset, setActivePreset] = useState<string | null>(null)
69
+ const [copied, setCopied] = useState(false)
70
+ const snippet = useMemo(() => renderSnippet(draft), [draft])
71
+
72
+ const slugValid = SLUG_RE.test(draft.slug)
73
+
74
+ const applyPreset = (preset: QuickPreset) => {
75
+ setActivePreset(preset.key)
76
+ setDraft({
77
+ slug: preset.slug,
78
+ singular: preset.singular,
79
+ plural: preset.plural,
80
+ color: preset.color,
81
+ icon: preset.icon,
82
+ description: preset.description,
83
+ extraFields: preset.extraFields,
84
+ })
85
+ }
86
+
87
+ // Manual edits clear the active-preset highlight but keep any
88
+ // preset-seeded extra fields (the user may still want them).
89
+ const patchDraft = (patch: Partial<DraftType>) => {
90
+ setActivePreset(null)
91
+ setDraft((d) => ({ ...d, ...patch }))
92
+ }
93
+
94
+ const copy = async () => {
95
+ try {
96
+ await navigator.clipboard.writeText(snippet)
97
+ setCopied(true)
98
+ toast.success('Snippet copied to clipboard')
99
+ setTimeout(() => setCopied(false), 1800)
100
+ } catch {
101
+ toast.error('Copy failed — select the snippet manually')
102
+ }
103
+ }
104
+
105
+ return (
106
+ <div className="mx-auto flex h-full max-w-3xl flex-col gap-6 p-4 sm:p-6">
107
+ <button
108
+ type="button"
109
+ onClick={() => onNavigate('/posts/types')}
110
+ className="text-muted-foreground hover:text-foreground inline-flex w-fit items-center gap-1.5 text-sm transition-colors"
111
+ >
112
+ <ArrowLeft className="h-3.5 w-3.5" aria-hidden />
113
+ Back to Post Types
114
+ </button>
115
+
116
+ <div className="flex flex-col gap-1">
117
+ <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
118
+ Add a Post Type
119
+ </h1>
120
+ <p className="text-muted-foreground text-sm">
121
+ Post Types are content models defined in{' '}
122
+ <code className="font-mono">actuate.config.ts</code>. Start from a quick template or fill
123
+ in the details below, then paste the generated snippet into your config and redeploy.
124
+ </p>
125
+ </div>
126
+
127
+ {/* Quick Post Type gallery */}
128
+ <section aria-labelledby="quick-types-heading" className="flex flex-col gap-3">
129
+ <div className="flex items-center gap-1.5">
130
+ <Sparkles className="text-muted-foreground h-3.5 w-3.5" aria-hidden />
131
+ <h2 id="quick-types-heading" className="text-foreground text-sm font-medium">
132
+ Quick start
133
+ </h2>
134
+ </div>
135
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
136
+ {POST_TYPE_PRESETS.map((preset) => {
137
+ const palette = paletteFor(preset.color)
138
+ const Icon = resolvePostTypeIcon(preset.icon)
139
+ const selected = activePreset === preset.key
140
+ return (
141
+ <button
142
+ key={preset.key}
143
+ type="button"
144
+ onClick={() => applyPreset(preset)}
145
+ aria-pressed={selected}
146
+ className={`focus-visible:ring-ring flex flex-col items-start gap-2 rounded-xl border p-3 text-left shadow-sm transition-colors focus:outline-none focus-visible:ring-2 ${
147
+ selected
148
+ ? 'border-primary bg-primary/5'
149
+ : 'border-border bg-card hover:border-primary/40 hover:bg-accent'
150
+ }`}
151
+ >
152
+ <span
153
+ className={`inline-flex h-8 w-8 items-center justify-center rounded-lg ${palette.bg} ${palette.text}`}
154
+ >
155
+ <Icon className="h-4 w-4" aria-hidden />
156
+ </span>
157
+ <span className="text-foreground text-sm leading-tight font-medium">
158
+ {preset.label}
159
+ </span>
160
+ </button>
161
+ )
162
+ })}
163
+ </div>
164
+ </section>
165
+
166
+ {/* Detail form */}
167
+ <div className="bg-card border-border grid grid-cols-1 gap-4 rounded-xl border p-5 shadow-sm sm:grid-cols-2">
168
+ <Field
169
+ label="Slug"
170
+ htmlFor="pt-slug"
171
+ error={draft.slug && !slugValid ? 'lowercase letters, numbers, and dashes only' : null}
172
+ >
173
+ <input
174
+ id="pt-slug"
175
+ value={draft.slug}
176
+ onChange={(e) => patchDraft({ slug: e.target.value.trim() })}
177
+ className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
178
+ placeholder="reviews"
179
+ />
180
+ </Field>
181
+ <Field label="Singular label" htmlFor="pt-singular">
182
+ <input
183
+ id="pt-singular"
184
+ value={draft.singular}
185
+ onChange={(e) => patchDraft({ singular: e.target.value })}
186
+ className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
187
+ placeholder="Review"
188
+ />
189
+ </Field>
190
+ <Field label="Plural label" htmlFor="pt-plural">
191
+ <input
192
+ id="pt-plural"
193
+ value={draft.plural}
194
+ onChange={(e) => patchDraft({ plural: e.target.value })}
195
+ className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
196
+ placeholder="Reviews"
197
+ />
198
+ </Field>
199
+ <Field label="Description" htmlFor="pt-desc">
200
+ <input
201
+ id="pt-desc"
202
+ value={draft.description}
203
+ onChange={(e) => patchDraft({ description: e.target.value })}
204
+ className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
205
+ placeholder="Product reviews and editorial pieces."
206
+ />
207
+ </Field>
208
+ <Field label="Accent color" htmlFor="pt-color">
209
+ <select
210
+ id="pt-color"
211
+ value={draft.color}
212
+ onChange={(e) => patchDraft({ color: e.target.value as PresetColor })}
213
+ className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
214
+ >
215
+ <option value="purple">Purple</option>
216
+ <option value="green">Green</option>
217
+ <option value="orange">Orange</option>
218
+ <option value="blue">Blue</option>
219
+ <option value="red">Red</option>
220
+ <option value="pink">Pink</option>
221
+ </select>
222
+ </Field>
223
+ <Field label="Icon" htmlFor="pt-icon">
224
+ <select
225
+ id="pt-icon"
226
+ value={draft.icon}
227
+ onChange={(e) => patchDraft({ icon: e.target.value as PostTypeIconName })}
228
+ className="border-border bg-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
229
+ >
230
+ {ICON_OPTIONS.map((o) => (
231
+ <option key={o.value} value={o.value}>
232
+ {o.label}
233
+ </option>
234
+ ))}
235
+ </select>
236
+ </Field>
237
+ </div>
238
+
239
+ <div>
240
+ <div className="mb-2 flex items-center justify-between">
241
+ <h2 className="text-foreground text-sm font-medium">
242
+ Add to <code className="font-mono">actuate.config.ts</code>
243
+ </h2>
244
+ <button
245
+ type="button"
246
+ onClick={copy}
247
+ disabled={!slugValid}
248
+ className="border-border bg-background text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40"
249
+ >
250
+ {copied ? (
251
+ <Check className="h-3.5 w-3.5" aria-hidden />
252
+ ) : (
253
+ <Copy className="h-3.5 w-3.5" aria-hidden />
254
+ )}
255
+ {copied ? 'Copied' : 'Copy snippet'}
256
+ </button>
257
+ </div>
258
+ <pre className="border-border bg-muted text-foreground overflow-x-auto rounded-lg border p-4 text-xs leading-relaxed">
259
+ <code>{snippet}</code>
260
+ </pre>
261
+ <p className="text-muted-foreground mt-3 text-xs">
262
+ After adding the snippet and redeploying, the new type will appear in the Posts area tabs
263
+ and on this Post Types page automatically.
264
+ </p>
265
+ </div>
266
+ </div>
267
+ )
268
+ }
269
+
270
+ function Field({
271
+ label,
272
+ htmlFor,
273
+ error,
274
+ children,
275
+ }: {
276
+ label: string
277
+ htmlFor: string
278
+ error?: string | null
279
+ children: React.ReactNode
280
+ }) {
281
+ return (
282
+ <div className="flex flex-col gap-1">
283
+ <label htmlFor={htmlFor} className="text-foreground text-sm font-medium">
284
+ {label}
285
+ </label>
286
+ {children}
287
+ {error ? <span className="text-destructive text-xs">{error}</span> : null}
288
+ </div>
289
+ )
290
+ }
291
+
292
+ export function renderSnippet(draft: DraftType): string {
293
+ const safeSlug = draft.slug || 'new-type'
294
+ const extraFieldsBlock =
295
+ draft.extraFields.length > 0
296
+ ? '\n // type-specific fields\n' +
297
+ draft.extraFields.map((line) => ` ${line}`).join('\n') +
298
+ '\n'
299
+ : ''
300
+
301
+ return `// Add this entry to the \`collections\` map in actuate.config.ts:
302
+ collections: {
303
+ // …existing collections,
304
+ '${safeSlug}': {
305
+ slug: '${safeSlug}',
306
+ labels: {
307
+ singular: '${escapeString(draft.singular || 'Entry')}',
308
+ plural: '${escapeString(draft.plural || 'Entries')}',
309
+ },
310
+ type: 'post',
311
+ urlPrefix: '${safeSlug}',
312
+ admin: {
313
+ group: 'Posts',
314
+ icon: '${draft.icon}',
315
+ color: '${draft.color}',
316
+ description: '${escapeString(draft.description)}',
317
+ },
318
+ fields: {
319
+ title: { type: 'text', label: 'Title', required: true },
320
+ slug: { type: 'slug', label: 'Slug', from: 'title' },
321
+ excerpt: { type: 'text', label: 'Excerpt' },
322
+ featuredImage: { type: 'media', label: 'Featured Image' },
323
+ body: { type: 'richText', label: 'Body' },
324
+ status: {
325
+ type: 'select',
326
+ label: 'Status',
327
+ options: [
328
+ { label: 'Draft', value: 'draft' },
329
+ { label: 'Published', value: 'published' },
330
+ { label: 'Scheduled', value: 'scheduled' },
331
+ ],
332
+ },
333
+ publishDate: { type: 'date', label: 'Publish Date', includeTime: true },
334
+ metaTitle: { type: 'text', label: 'SEO Title' },
335
+ metaDescription: { type: 'text', label: 'SEO Description' },
336
+ ${extraFieldsBlock} },
337
+ },
338
+ },
339
+ `
340
+ }
341
+
342
+ function escapeString(input: string): string {
343
+ return input.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
344
+ }
@@ -0,0 +1,194 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { LayoutTemplate, Plus } from 'lucide-react'
5
+ import { fetchPosts, type PostType } from '../../lib/posts-service.js'
6
+ import { paletteFor } from '../../components/posts/badges.js'
7
+ import { resolvePostTypeIcon } from '../../components/posts/icons.js'
8
+
9
+ /**
10
+ * Post Types — card grid that mirrors the reference screenshot. Each
11
+ * card surfaces:
12
+ * - colored top border (accent)
13
+ * - icon badge in upper-left
14
+ * - title + description
15
+ * - footer row: post count · field count · URL base
16
+ *
17
+ * Cards open the per-collection settings (`/collections/:slug/settings`
18
+ * in the existing admin) when clicked. The dashed "+ New Post Type"
19
+ * card routes to /posts/types/new where users get instructions for
20
+ * adding a new collection to the config file.
21
+ *
22
+ * Why no in-app create/edit flow yet:
23
+ * Collections are file-defined (actuate.config.ts). Writing to that
24
+ * file at runtime would require a parallel data model and break the
25
+ * read-only deploy invariant on serverless platforms. v2 of this
26
+ * surface will introduce a DB-overlay for visual metadata (icon /
27
+ * color / description) — schema stays in code.
28
+ */
29
+
30
+ interface PostTypesViewProps {
31
+ onNavigate: (path: string) => void
32
+ }
33
+
34
+ export function PostTypesView({ onNavigate }: PostTypesViewProps) {
35
+ const [types, setTypes] = useState<PostType[]>([])
36
+ const [loading, setLoading] = useState(true)
37
+ const [error, setError] = useState<string | null>(null)
38
+ const [refreshToken, setRefreshToken] = useState(0)
39
+
40
+ useEffect(() => {
41
+ let cancelled = false
42
+ setLoading(true)
43
+ setError(null)
44
+ fetchPosts({ pageSize: 1 })
45
+ .then((res) => {
46
+ if (cancelled) return
47
+ setTypes(res.types)
48
+ setLoading(false)
49
+ })
50
+ .catch((err: unknown) => {
51
+ if (cancelled) return
52
+ setError(err instanceof Error ? err.message : 'Failed to load Post Types')
53
+ setLoading(false)
54
+ })
55
+ return () => {
56
+ cancelled = true
57
+ }
58
+ }, [refreshToken])
59
+
60
+ return (
61
+ <div className="flex h-full flex-col gap-6 p-4 sm:p-6">
62
+ <div className="flex flex-col gap-1">
63
+ <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
64
+ Post Types
65
+ </h1>
66
+ <p className="text-muted-foreground text-sm">
67
+ Define content models and their fields. All types share the same default fields.
68
+ </p>
69
+ </div>
70
+
71
+ {loading && <CardGridSkeleton />}
72
+ {!loading && error && (
73
+ <div className="border-border bg-card rounded-xl border p-8 text-center">
74
+ <div className="text-foreground mb-2 text-sm font-medium">{error}</div>
75
+ <button
76
+ type="button"
77
+ onClick={() => setRefreshToken((t) => t + 1)}
78
+ className="text-primary text-sm hover:underline"
79
+ >
80
+ Try again
81
+ </button>
82
+ </div>
83
+ )}
84
+
85
+ {!loading && !error && (
86
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
87
+ {types.map((t) => (
88
+ <PostTypeCard
89
+ key={t.slug}
90
+ type={t}
91
+ onClick={() => onNavigate(`/posts/types/${t.slug}/edit`)}
92
+ onEditTemplate={() => onNavigate(`/posts/types/${t.slug}/template`)}
93
+ />
94
+ ))}
95
+ <NewPostTypeCard onClick={() => onNavigate('/posts/types/new')} />
96
+ </div>
97
+ )}
98
+ </div>
99
+ )
100
+ }
101
+
102
+ // ─── PostTypeCard ────────────────────────────────────────────────────
103
+
104
+ function PostTypeCard({
105
+ type,
106
+ onClick,
107
+ onEditTemplate,
108
+ }: {
109
+ type: PostType
110
+ onClick: () => void
111
+ onEditTemplate: () => void
112
+ }) {
113
+ const palette = paletteFor(type.accentColor)
114
+ const Icon = resolvePostTypeIcon(type.icon)
115
+ // Tailwind needs the exact class string at build time — derive the
116
+ // top-border color from the palette's `border` token directly so it
117
+ // stays in sync with the badge color.
118
+ const topBorderClass = palette.border
119
+
120
+ return (
121
+ <div className="bg-card border-border relative flex flex-col gap-3 overflow-hidden rounded-xl border p-5 shadow-sm transition-shadow hover:shadow-md">
122
+ <span aria-hidden className={`absolute inset-x-0 top-0 h-1 ${topBorderClass}`} />
123
+ {/* Main click target: manage posts of this type. */}
124
+ <button
125
+ type="button"
126
+ onClick={onClick}
127
+ aria-label={`Manage ${type.name} posts`}
128
+ className="focus-visible:ring-ring -m-1 flex flex-col gap-3 rounded-lg p-1 text-left focus:outline-none focus-visible:ring-2"
129
+ >
130
+ <div className="flex items-center gap-3 pt-2">
131
+ <span
132
+ className={`inline-flex h-9 w-9 items-center justify-center rounded-lg ${palette.bg} ${palette.text}`}
133
+ >
134
+ <Icon className="h-4 w-4" aria-hidden />
135
+ </span>
136
+ <h2 className="text-foreground text-base leading-tight font-medium">{type.name}</h2>
137
+ </div>
138
+ <p className="text-muted-foreground line-clamp-2 text-sm">
139
+ {type.description ?? 'No description provided.'}
140
+ </p>
141
+ </button>
142
+ <div className="border-border mt-auto flex flex-wrap items-center gap-x-4 gap-y-1 border-t pt-3 text-xs">
143
+ <span className="text-foreground font-medium tabular-nums">
144
+ {type.postCount} <span className="text-muted-foreground font-normal">posts</span>
145
+ </span>
146
+ <span className="text-foreground font-medium tabular-nums">
147
+ {type.fieldCount} <span className="text-muted-foreground font-normal">fields</span>
148
+ </span>
149
+ <span className="text-muted-foreground ml-auto truncate font-mono">{type.urlBase}</span>
150
+ </div>
151
+ <button
152
+ type="button"
153
+ onClick={onEditTemplate}
154
+ className="border-border text-foreground hover:bg-accent focus-visible:ring-ring inline-flex items-center justify-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2"
155
+ >
156
+ <LayoutTemplate className="h-3.5 w-3.5" aria-hidden />
157
+ Edit template
158
+ </button>
159
+ </div>
160
+ )
161
+ }
162
+
163
+ function NewPostTypeCard({ onClick }: { onClick: () => void }) {
164
+ return (
165
+ <button
166
+ type="button"
167
+ onClick={onClick}
168
+ aria-label="New Post Type"
169
+ className="border-border text-muted-foreground hover:text-foreground hover:border-primary/50 hover:bg-primary/5 focus-visible:ring-ring flex min-h-[170px] flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed bg-transparent p-5 transition-colors focus:outline-none focus-visible:ring-2"
170
+ >
171
+ <span className="bg-muted text-muted-foreground inline-flex h-9 w-9 items-center justify-center rounded-full">
172
+ <Plus className="h-4 w-4" aria-hidden />
173
+ </span>
174
+ <span className="text-sm font-medium">New Post Type</span>
175
+ </button>
176
+ )
177
+ }
178
+
179
+ function CardGridSkeleton() {
180
+ return (
181
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
182
+ {Array.from({ length: 4 }).map((_, i) => (
183
+ <div
184
+ key={i}
185
+ className="bg-card border-border min-h-[170px] animate-pulse rounded-xl border p-5"
186
+ >
187
+ <div className="bg-muted mb-3 h-9 w-9 rounded-lg" />
188
+ <div className="bg-muted mb-2 h-4 w-1/2 rounded" />
189
+ <div className="bg-muted h-3 w-3/4 rounded" />
190
+ </div>
191
+ ))}
192
+ </div>
193
+ )
194
+ }