@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,63 @@
1
+ import { cmsApi } from './api.js'
2
+
3
+ /**
4
+ * Mint a signed, time-boxed preview token for a single document. The token is
5
+ * bound to the (collection, documentId) pair server-side, so it can only
6
+ * unlock the exact draft it was issued for. Used to build public preview URLs
7
+ * (`/<prefix>/<slug>?preview=<token>`) that render a draft through the
8
+ * consumer's real front-end — see {@link buildPublicPreviewUrl}.
9
+ */
10
+ export async function createPreviewToken(
11
+ collection: string,
12
+ documentId: string,
13
+ ttlSeconds?: number,
14
+ ): Promise<{ token: string; expiresAt: string; slug?: string | null }> {
15
+ const res = await cmsApi<{ token: string; expiresAt: string; slug?: string | null }>(
16
+ '/preview/token',
17
+ {
18
+ method: 'POST',
19
+ body: JSON.stringify({ collection, documentId, ttlSeconds }),
20
+ },
21
+ )
22
+ if (res.error || !res.data) {
23
+ throw new Error(res.error ?? 'Failed to create preview token')
24
+ }
25
+ return res.data
26
+ }
27
+
28
+ export interface PublicPreviewUrlParts {
29
+ /** Absolute site origin, e.g. `https://example.com`. Trailing slash optional. */
30
+ siteUrl: string
31
+ /** Collection URL prefix (e.g. `blog`). Empty for top-level page collections. */
32
+ urlPrefix?: string | null
33
+ /** Document slug used in the public path. */
34
+ slug: string
35
+ /** Signed preview token from {@link createPreviewToken}. */
36
+ token: string
37
+ }
38
+
39
+ /**
40
+ * Build the public URL a draft preview opens at — the SAME path the
41
+ * `/resolve` endpoint matches: `<urlPrefix>/<slug>` (or `/<slug>` for
42
+ * top-level pages), with the preview token on the query string so the
43
+ * public route can drop the `PUBLISHED` filter for this one document.
44
+ */
45
+ export function buildPublicPreviewUrl({
46
+ siteUrl,
47
+ urlPrefix,
48
+ slug,
49
+ token,
50
+ }: PublicPreviewUrlParts): string {
51
+ const base = (siteUrl ?? '').replace(/\/+$/, '')
52
+ const prefix = (urlPrefix ?? '').replace(/^\/+|\/+$/g, '')
53
+ const cleanSlug = slug.replace(/^\/+|\/+$/g, '')
54
+ const path = prefix ? `/${prefix}/${cleanSlug}` : `/${cleanSlug}`
55
+ return `${base}${path}?preview=${encodeURIComponent(token)}`
56
+ }
57
+
58
+ /** Resolve the public site origin: explicit config first, else the current tab's origin. */
59
+ export function resolveSiteUrl(configSiteUrl?: string | null): string {
60
+ if (configSiteUrl && configSiteUrl.trim()) return configSiteUrl.trim()
61
+ if (typeof window !== 'undefined') return window.location.origin
62
+ return ''
63
+ }
@@ -44,6 +44,15 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
44
44
  window.history.pushState({ adminPath: normalizedPath }, '', fullUrl)
45
45
  }, [])
46
46
 
47
+ // Build an absolute (basePath-prefixed) admin URL without navigating. Used
48
+ // for links that must load a real document — e.g. opening a preview in a
49
+ // new browser tab, where `pushState` (which only mutates the current tab)
50
+ // is not an option.
51
+ const buildHref = useCallback((path: string) => {
52
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`
53
+ return normalizedPath === '/' ? baseRef.current : `${baseRef.current}${normalizedPath}`
54
+ }, [])
55
+
47
56
  const matchRoute = useCallback(
48
57
  (pattern: string): RouteParams | null => {
49
58
  const patternParts = pattern.split('/').filter(Boolean)
@@ -75,5 +84,5 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
75
84
  [currentPath],
76
85
  )
77
86
 
78
- return { currentPath, navigate, matchRoute }
87
+ return { currentPath, navigate, matchRoute, buildHref }
79
88
  }
@@ -15,7 +15,7 @@
15
15
  * >= 1024 (desktop): stat cards 5-col, main grid is `1fr 320px`
16
16
  */
17
17
 
18
- import { useMemo, useState } from 'react'
18
+ import { useEffect, useMemo, useState } from 'react'
19
19
  import {
20
20
  FileText,
21
21
  File as FileIcon,
@@ -119,6 +119,24 @@ function collectionLabel(col: CollectionMeta, plural = true): string {
119
119
  return col.labels?.singular ?? fallback
120
120
  }
121
121
 
122
+ /**
123
+ * Resolve the correct editor route for a recent/queued document.
124
+ *
125
+ * Post-type documents MUST open in the section-based post editor
126
+ * (`/posts/:type/:id/edit`). Routing them through the generic
127
+ * `/:collection/:id` handler lands them in the legacy `DocumentEdit`
128
+ * view, which has no concept of the section model — opening + saving
129
+ * there silently strips a post's `sections` data. The canonical Pages
130
+ * collection opens in the full page editor (which wires up preview).
131
+ * Everything else falls through to the existing generic collection
132
+ * route, which already resolves correctly.
133
+ */
134
+ function editPathForDoc(col: CollectionMeta | undefined, collection: string, id: string): string {
135
+ if (col?.type === 'post') return `/posts/${collection}/${id}/edit`
136
+ if (collection === 'pages') return `/pages/${id}/edit`
137
+ return `/${collection}/${id}`
138
+ }
139
+
122
140
  function relativeTime(dateStr: string): string {
123
141
  const then = new Date(dateStr).getTime()
124
142
  const diff = Date.now() - then
@@ -252,8 +270,18 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
252
270
  const { data: contentHealthData, loading: contentHealthLoading } =
253
271
  useApiData<ContentHealthData>('/stats/content-health')
254
272
 
255
- const greeting = useMemo(() => timeOfDayGreeting(), [])
256
- const dateStr = useMemo(() => todayDateString(), [])
273
+ // A slow ticker (60s) so the greeting, calendar date, and "x minutes
274
+ // ago" relative times stay correct on a dashboard left open for hours
275
+ // — without it, a tab opened at 11:58am would read "Good morning" all
276
+ // afternoon and show stale activity timestamps.
277
+ const [now, setNow] = useState(() => Date.now())
278
+ useEffect(() => {
279
+ const id = setInterval(() => setNow(Date.now()), 60_000)
280
+ return () => clearInterval(id)
281
+ }, [])
282
+
283
+ const greeting = useMemo(() => timeOfDayGreeting(), [now])
284
+ const dateStr = useMemo(() => todayDateString(), [now])
257
285
  const userName = useMemo(() => firstNameForGreeting(session), [session])
258
286
 
259
287
  const collections = useMemo(() => resolveCollections(config), [config])
@@ -358,12 +386,24 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
358
386
  // dashboard's content-creation toolbar. Order mirrors the natural
359
387
  // authoring flow (post → page → media → form → integration).
360
388
  const quickActions = useMemo(() => {
361
- const posts = collections.find((c) => c.slug === 'posts' || c.type === 'post')
362
- const postsSlug = posts?.slug ?? 'posts'
389
+ const postTypes = collections.filter((c) => c.type === 'post')
363
390
  const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page')
364
391
  const pagesSlug = pages?.slug ?? 'pages'
392
+ // "New Post": one Post Type → straight to its editor; several →
393
+ // the picker; none → the Post Type setup instructions (avoids a
394
+ // broken slug-based new URL).
395
+ const newPostPath =
396
+ postTypes.length === 1
397
+ ? `/${postTypes[0]!.slug}/new`
398
+ : postTypes.length > 1
399
+ ? '/posts/new'
400
+ : '/posts/types/new'
365
401
  const items: { label: string; icon: LucideIcon; onClick: () => void }[] = [
366
- { label: 'New Post', icon: Plus, onClick: () => nav(`/${postsSlug}/new`) },
402
+ {
403
+ label: 'New Post',
404
+ icon: Plus,
405
+ onClick: () => nav(newPostPath),
406
+ },
367
407
  { label: 'New Page', icon: Plus, onClick: () => nav(`/${pagesSlug}/new`) },
368
408
  { label: 'Upload Media', icon: Upload, onClick: () => nav('/media') },
369
409
  { label: 'New Form', icon: Plus, onClick: () => nav('/forms') },
@@ -384,9 +424,12 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
384
424
  relTime: relativeTime(d.updatedAt),
385
425
  avatar: authorAvatar(d.author),
386
426
  statusInfo: statusBadge(d.status),
427
+ editPath: editPathForDoc(col, d.collection, d.id),
387
428
  }
388
429
  })
389
- }, [stats, collections, activityLimit])
430
+ // `now` keeps the relative timestamps fresh on a long-open tab.
431
+ // eslint-disable-next-line react-hooks/exhaustive-deps
432
+ }, [stats, collections, activityLimit, now])
390
433
 
391
434
  // ── Publishing queue (scheduled docs only) ──────────────────────────────
392
435
  const publishQueue = useMemo(() => {
@@ -403,9 +446,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
403
446
  type: col ? collectionLabel(col, false) : d.collection,
404
447
  date: relativeTime(d.updatedAt),
405
448
  author: d.author,
449
+ editPath: editPathForDoc(col, d.collection, d.id),
406
450
  }
407
451
  })
408
- }, [stats, collections])
452
+ // eslint-disable-next-line react-hooks/exhaustive-deps
453
+ }, [stats, collections, now])
409
454
 
410
455
  // ── Content health (sourced from /stats/content-health) ─────────────────
411
456
  // Persisted by the nightly content-health cron. The four issue rows
@@ -606,12 +651,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
606
651
  <h2 className="text-foreground text-sm font-semibold">Recent Activity</h2>
607
652
  <p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
608
653
  </div>
609
- <button
610
- className="text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
611
- onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
612
- >
613
- {activity.length >= 20 ? 'Show less' : 'View all'}
614
- </button>
654
+ {(stats?.recentDocuments?.length ?? 0) > 8 && (
655
+ <button
656
+ className="text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
657
+ onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
658
+ >
659
+ {activityLimit >= 20 ? 'Show less' : 'Show more'}
660
+ </button>
661
+ )}
615
662
  </header>
616
663
  {activity.length === 0 ? (
617
664
  <EmptyState
@@ -622,38 +669,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
622
669
  ) : (
623
670
  <ul role="list" className="divide-border divide-y">
624
671
  {activity.map((it) => (
625
- <li
626
- key={it.id}
627
- className="hover:bg-accent/50 cursor-pointer px-4 py-3 transition-colors"
628
- onClick={() => nav(`/${it.collection}/${it.id}`)}
629
- >
630
- <div className="flex min-w-0 items-start gap-3">
631
- <div
632
- className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
633
- style={{ background: it.avatar.color }}
634
- aria-hidden
635
- >
636
- {it.avatar.initials}
637
- </div>
638
- <div className="min-w-0 flex-1">
639
- <p className="text-foreground truncate text-sm leading-snug">
640
- <span className="font-semibold">
641
- &ldquo;{it.title || 'Untitled'}&rdquo;
642
- </span>{' '}
643
- <span className="text-muted-foreground">— {it.typeLabel}</span>
644
- </p>
645
- <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
646
- <span
647
- className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
648
- >
649
- {it.statusInfo.label}
650
- </span>
651
- <span className="max-w-[120px] truncate">{it.author}</span>
652
- <span aria-hidden>·</span>
653
- <span>{it.relTime}</span>
672
+ <li key={it.id}>
673
+ <button
674
+ type="button"
675
+ onClick={() => nav(it.editPath)}
676
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
677
+ >
678
+ <div className="flex min-w-0 items-start gap-3">
679
+ <div
680
+ className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
681
+ style={{ background: it.avatar.color }}
682
+ aria-hidden
683
+ >
684
+ {it.avatar.initials}
685
+ </div>
686
+ <div className="min-w-0 flex-1">
687
+ <p className="text-foreground truncate text-sm leading-snug">
688
+ <span className="font-semibold">
689
+ &ldquo;{it.title || 'Untitled'}&rdquo;
690
+ </span>{' '}
691
+ <span className="text-muted-foreground">— {it.typeLabel}</span>
692
+ </p>
693
+ <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
694
+ <span
695
+ className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
696
+ >
697
+ {it.statusInfo.label}
698
+ </span>
699
+ <span className="max-w-[120px] truncate">{it.author}</span>
700
+ <span aria-hidden>·</span>
701
+ <span>{it.relTime}</span>
702
+ </div>
654
703
  </div>
655
704
  </div>
656
- </div>
705
+ </button>
657
706
  </li>
658
707
  ))}
659
708
  </ul>
@@ -679,7 +728,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
679
728
  // link to the first scheduled item — that's where authors
680
729
  // typically need to land to reschedule or cancel.
681
730
  const first = publishQueue[0]
682
- if (first) nav(`/${first.collection}/${first.id}`)
731
+ if (first) nav(first.editPath)
683
732
  }}
684
733
  >
685
734
  Manage
@@ -695,21 +744,23 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
695
744
  ) : (
696
745
  <ul role="list" className="divide-border divide-y">
697
746
  {publishQueue.map((q) => (
698
- <li
699
- key={q.id}
700
- className="hover:bg-accent/50 flex min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 transition-colors"
701
- onClick={() => nav(`/${q.collection}/${q.id}`)}
702
- >
703
- <span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" aria-hidden />
704
- <div className="min-w-0 flex-1">
705
- <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
706
- <p className="text-muted-foreground truncate text-[11px]">
707
- {q.date} · {q.author}
708
- </p>
709
- </div>
710
- <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
711
- {q.type}
712
- </span>
747
+ <li key={q.id}>
748
+ <button
749
+ type="button"
750
+ onClick={() => nav(q.editPath)}
751
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 text-left transition-colors"
752
+ >
753
+ <span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" aria-hidden />
754
+ <div className="min-w-0 flex-1">
755
+ <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
756
+ <p className="text-muted-foreground truncate text-[11px]">
757
+ {q.date} · {q.author}
758
+ </p>
759
+ </div>
760
+ <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
761
+ {q.type}
762
+ </span>
763
+ </button>
713
764
  </li>
714
765
  ))}
715
766
  </ul>
@@ -774,36 +825,38 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
774
825
  ) : (
775
826
  <ul role="list" className="divide-border divide-y">
776
827
  {contentHealth.issues.map((iss, i) => (
777
- <li
778
- key={i}
779
- className="hover:bg-accent/50 flex cursor-pointer items-center gap-2.5 px-4 py-2 transition-colors"
780
- onClick={() => nav('/seo')}
781
- >
782
- <span
783
- className={`h-1.5 w-1.5 shrink-0 rounded-full ${
784
- iss.tone === 'err'
785
- ? 'bg-red-500'
786
- : iss.tone === 'warn'
787
- ? 'bg-amber-500'
788
- : 'bg-muted-foreground'
789
- }`}
790
- aria-hidden
791
- />
792
- <span className="text-muted-foreground flex-1 truncate text-xs">
793
- {iss.label}
794
- </span>
795
- <span
796
- className={`shrink-0 text-sm font-semibold ${
797
- iss.tone === 'err'
798
- ? 'text-red-600 dark:text-red-400'
799
- : iss.tone === 'warn'
800
- ? 'text-amber-600 dark:text-amber-400'
801
- : 'text-muted-foreground'
802
- }`}
828
+ <li key={i}>
829
+ <button
830
+ type="button"
831
+ onClick={() => nav('/seo')}
832
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full items-center gap-2.5 px-4 py-2 text-left transition-colors"
803
833
  >
804
- {iss.count}
805
- </span>
806
- <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
834
+ <span
835
+ className={`h-1.5 w-1.5 shrink-0 rounded-full ${
836
+ iss.tone === 'err'
837
+ ? 'bg-red-500'
838
+ : iss.tone === 'warn'
839
+ ? 'bg-amber-500'
840
+ : 'bg-muted-foreground'
841
+ }`}
842
+ aria-hidden
843
+ />
844
+ <span className="text-muted-foreground flex-1 truncate text-xs">
845
+ {iss.label}
846
+ </span>
847
+ <span
848
+ className={`shrink-0 text-sm font-semibold ${
849
+ iss.tone === 'err'
850
+ ? 'text-red-600 dark:text-red-400'
851
+ : iss.tone === 'warn'
852
+ ? 'text-amber-600 dark:text-amber-400'
853
+ : 'text-muted-foreground'
854
+ }`}
855
+ >
856
+ {iss.count}
857
+ </span>
858
+ <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
859
+ </button>
807
860
  </li>
808
861
  ))}
809
862
  </ul>