@actuate-media/cms-admin 0.9.0 → 0.11.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 (294) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +8 -5
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/layout/primitives.test.d.ts +2 -0
  5. package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
  6. package/dist/__tests__/layout/primitives.test.js +34 -0
  7. package/dist/__tests__/layout/primitives.test.js.map +1 -0
  8. package/dist/__tests__/lib/cv.test.d.ts +2 -0
  9. package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
  10. package/dist/__tests__/lib/cv.test.js +66 -0
  11. package/dist/__tests__/lib/cv.test.js.map +1 -0
  12. package/dist/actuate-admin.css +1 -1
  13. package/dist/assets/actuate-logo.d.ts +36 -0
  14. package/dist/assets/actuate-logo.d.ts.map +1 -0
  15. package/dist/assets/actuate-logo.js +15 -0
  16. package/dist/assets/actuate-logo.js.map +1 -0
  17. package/dist/components/Breadcrumbs.js +2 -2
  18. package/dist/components/CommandPalette.js +10 -10
  19. package/dist/components/ContentOverviewChart.js +3 -3
  20. package/dist/components/ErrorBoundary.js +1 -1
  21. package/dist/components/FocalPointPicker.js +2 -2
  22. package/dist/components/FolderTree.js +20 -20
  23. package/dist/components/LivePreview.js +3 -3
  24. package/dist/components/LocaleSwitcher.js +1 -1
  25. package/dist/components/MediaPickerModal.js +4 -4
  26. package/dist/components/PresenceIndicator.js +1 -1
  27. package/dist/components/SEOConfigPanel.d.ts +2 -0
  28. package/dist/components/SEOConfigPanel.d.ts.map +1 -0
  29. package/dist/components/SEOConfigPanel.js +174 -0
  30. package/dist/components/SEOConfigPanel.js.map +1 -0
  31. package/dist/components/SEOPanel.js +9 -9
  32. package/dist/components/SEOPerformance.js +2 -2
  33. package/dist/components/SchedulePublishDialog.d.ts +18 -0
  34. package/dist/components/SchedulePublishDialog.d.ts.map +1 -0
  35. package/dist/components/SchedulePublishDialog.js +106 -0
  36. package/dist/components/SchedulePublishDialog.js.map +1 -0
  37. package/dist/components/SharePreviewLinkDialog.d.ts +17 -0
  38. package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -0
  39. package/dist/components/SharePreviewLinkDialog.js +83 -0
  40. package/dist/components/SharePreviewLinkDialog.js.map +1 -0
  41. package/dist/components/TipTapEditor.js +5 -5
  42. package/dist/components/VersionHistory.js +2 -2
  43. package/dist/components/ui/Badge.d.ts +33 -3
  44. package/dist/components/ui/Badge.d.ts.map +1 -1
  45. package/dist/components/ui/Badge.js +42 -8
  46. package/dist/components/ui/Badge.js.map +1 -1
  47. package/dist/components/ui/Button.d.ts +19 -8
  48. package/dist/components/ui/Button.d.ts.map +1 -1
  49. package/dist/components/ui/Button.js +35 -14
  50. package/dist/components/ui/Button.js.map +1 -1
  51. package/dist/components/ui/Card.d.ts +26 -0
  52. package/dist/components/ui/Card.d.ts.map +1 -0
  53. package/dist/components/ui/Card.js +45 -0
  54. package/dist/components/ui/Card.js.map +1 -0
  55. package/dist/components/ui/DataTable.js +1 -1
  56. package/dist/components/ui/Input.d.ts +15 -0
  57. package/dist/components/ui/Input.d.ts.map +1 -0
  58. package/dist/components/ui/Input.js +23 -0
  59. package/dist/components/ui/Input.js.map +1 -0
  60. package/dist/components/ui/SearchInput.js +1 -1
  61. package/dist/components/ui/Select.d.ts +16 -0
  62. package/dist/components/ui/Select.d.ts.map +1 -0
  63. package/dist/components/ui/Select.js +25 -0
  64. package/dist/components/ui/Select.js.map +1 -0
  65. package/dist/components/ui/Toast.js +1 -1
  66. package/dist/components/ui/index.d.ts +10 -4
  67. package/dist/components/ui/index.d.ts.map +1 -1
  68. package/dist/components/ui/index.js +5 -2
  69. package/dist/components/ui/index.js.map +1 -1
  70. package/dist/fields/BlockBuilderField.js +3 -3
  71. package/dist/fields/DateField.js +1 -1
  72. package/dist/fields/RelationshipField.js +3 -3
  73. package/dist/fields/TextField.js +1 -1
  74. package/dist/index.d.ts +6 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/layout/Header.js +1 -1
  79. package/dist/layout/Layout.d.ts +14 -0
  80. package/dist/layout/Layout.d.ts.map +1 -1
  81. package/dist/layout/Layout.js +17 -11
  82. package/dist/layout/Layout.js.map +1 -1
  83. package/dist/layout/Sidebar.d.ts.map +1 -1
  84. package/dist/layout/Sidebar.js +21 -11
  85. package/dist/layout/Sidebar.js.map +1 -1
  86. package/dist/layout/primitives/AdminShell.d.ts +43 -0
  87. package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
  88. package/dist/layout/primitives/AdminShell.js +51 -0
  89. package/dist/layout/primitives/AdminShell.js.map +1 -0
  90. package/dist/layout/primitives/Box.d.ts +19 -0
  91. package/dist/layout/primitives/Box.d.ts.map +1 -0
  92. package/dist/layout/primitives/Box.js +12 -0
  93. package/dist/layout/primitives/Box.js.map +1 -0
  94. package/dist/layout/primitives/Cluster.d.ts +27 -0
  95. package/dist/layout/primitives/Cluster.d.ts.map +1 -0
  96. package/dist/layout/primitives/Cluster.js +37 -0
  97. package/dist/layout/primitives/Cluster.js.map +1 -0
  98. package/dist/layout/primitives/Grid.d.ts +45 -0
  99. package/dist/layout/primitives/Grid.d.ts.map +1 -0
  100. package/dist/layout/primitives/Grid.js +59 -0
  101. package/dist/layout/primitives/Grid.js.map +1 -0
  102. package/dist/layout/primitives/PageContainer.d.ts +36 -0
  103. package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
  104. package/dist/layout/primitives/PageContainer.js +41 -0
  105. package/dist/layout/primitives/PageContainer.js.map +1 -0
  106. package/dist/layout/primitives/Split.d.ts +34 -0
  107. package/dist/layout/primitives/Split.d.ts.map +1 -0
  108. package/dist/layout/primitives/Split.js +27 -0
  109. package/dist/layout/primitives/Split.js.map +1 -0
  110. package/dist/layout/primitives/Stack.d.ts +23 -0
  111. package/dist/layout/primitives/Stack.d.ts.map +1 -0
  112. package/dist/layout/primitives/Stack.js +34 -0
  113. package/dist/layout/primitives/Stack.js.map +1 -0
  114. package/dist/layout/primitives/index.d.ts +30 -0
  115. package/dist/layout/primitives/index.d.ts.map +1 -0
  116. package/dist/layout/primitives/index.js +22 -0
  117. package/dist/layout/primitives/index.js.map +1 -0
  118. package/dist/layout/primitives/tokens.d.ts +48 -0
  119. package/dist/layout/primitives/tokens.d.ts.map +1 -0
  120. package/dist/layout/primitives/tokens.js +54 -0
  121. package/dist/layout/primitives/tokens.js.map +1 -0
  122. package/dist/lib/cv.d.ts +53 -0
  123. package/dist/lib/cv.d.ts.map +1 -0
  124. package/dist/lib/cv.js +39 -0
  125. package/dist/lib/cv.js.map +1 -0
  126. package/dist/views/ApiKeys.d.ts.map +1 -1
  127. package/dist/views/ApiKeys.js +13 -11
  128. package/dist/views/ApiKeys.js.map +1 -1
  129. package/dist/views/CollectionList.js +8 -8
  130. package/dist/views/Dashboard.d.ts.map +1 -1
  131. package/dist/views/Dashboard.js +333 -78
  132. package/dist/views/Dashboard.js.map +1 -1
  133. package/dist/views/DocumentEdit.d.ts.map +1 -1
  134. package/dist/views/DocumentEdit.js +17 -5
  135. package/dist/views/DocumentEdit.js.map +1 -1
  136. package/dist/views/ForgotPassword.js +2 -2
  137. package/dist/views/FormEditor.js +5 -5
  138. package/dist/views/FormSubmissions.js +6 -6
  139. package/dist/views/Forms.js +2 -2
  140. package/dist/views/Login.d.ts +16 -1
  141. package/dist/views/Login.d.ts.map +1 -1
  142. package/dist/views/Login.js +17 -7
  143. package/dist/views/Login.js.map +1 -1
  144. package/dist/views/MediaBrowser.js +16 -16
  145. package/dist/views/PageEditor.js +2 -2
  146. package/dist/views/Pages.js +10 -10
  147. package/dist/views/PostEditor.js +2 -2
  148. package/dist/views/Posts.js +4 -4
  149. package/dist/views/Redirects.js +4 -4
  150. package/dist/views/ResetPassword.js +2 -2
  151. package/dist/views/SEO.js +6 -6
  152. package/dist/views/ScriptTagEditor.js +4 -4
  153. package/dist/views/ScriptTags.js +2 -2
  154. package/dist/views/Settings.d.ts.map +1 -1
  155. package/dist/views/Settings.js +9 -8
  156. package/dist/views/Settings.js.map +1 -1
  157. package/dist/views/SetupWizard.js +2 -2
  158. package/dist/views/Users.js +4 -4
  159. package/dist/views/page-builder/AIBlockAssist.js +1 -1
  160. package/dist/views/page-builder/AIGenerateDialog.js +10 -10
  161. package/dist/views/page-builder/BlockEditor.js +10 -10
  162. package/dist/views/page-builder/BlockPicker.js +4 -4
  163. package/dist/views/page-builder/BottomBar.js +1 -1
  164. package/dist/views/page-builder/BuilderToolbar.js +2 -2
  165. package/dist/views/page-builder/ContextPanel.js +2 -2
  166. package/dist/views/page-builder/DesignScore.js +9 -9
  167. package/dist/views/page-builder/NodeSettings.js +8 -8
  168. package/dist/views/page-builder/PageBuilder.js +3 -3
  169. package/dist/views/page-builder/PageSettings.js +1 -1
  170. package/dist/views/page-builder/PageTemplates.js +2 -2
  171. package/dist/views/page-builder/SEOPanel.js +13 -13
  172. package/dist/views/page-builder/SavedSections.js +5 -5
  173. package/dist/views/page-builder/TemplatePicker.js +2 -2
  174. package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
  175. package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
  176. package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
  177. package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
  178. package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
  179. package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
  180. package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
  181. package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
  182. package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
  183. package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
  184. package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
  185. package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
  186. package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
  187. package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
  188. package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
  189. package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
  190. package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
  191. package/package.json +6 -2
  192. package/src/AdminRoot.tsx +21 -11
  193. package/src/__tests__/layout/primitives.test.ts +37 -0
  194. package/src/__tests__/lib/cv.test.ts +74 -0
  195. package/src/assets/actuate-logo.tsx +72 -0
  196. package/src/components/Breadcrumbs.tsx +6 -6
  197. package/src/components/CommandPalette.tsx +34 -34
  198. package/src/components/ContentOverviewChart.tsx +3 -3
  199. package/src/components/ErrorBoundary.tsx +3 -3
  200. package/src/components/FocalPointPicker.tsx +4 -4
  201. package/src/components/FolderTree.tsx +38 -38
  202. package/src/components/LivePreview.tsx +16 -16
  203. package/src/components/LocaleSwitcher.tsx +7 -7
  204. package/src/components/MediaPickerModal.tsx +21 -21
  205. package/src/components/PresenceIndicator.tsx +2 -2
  206. package/src/components/SEOConfigPanel.tsx +582 -0
  207. package/src/components/SEOPanel.tsx +46 -46
  208. package/src/components/SEOPerformance.tsx +21 -21
  209. package/src/components/SchedulePublishDialog.tsx +241 -0
  210. package/src/components/SharePreviewLinkDialog.tsx +227 -0
  211. package/src/components/TipTapEditor.tsx +33 -33
  212. package/src/components/VersionHistory.tsx +16 -16
  213. package/src/components/ui/Badge.tsx +66 -14
  214. package/src/components/ui/Button.tsx +70 -33
  215. package/src/components/ui/Card.tsx +101 -0
  216. package/src/components/ui/DataTable.tsx +1 -1
  217. package/src/components/ui/Input.tsx +35 -0
  218. package/src/components/ui/SearchInput.tsx +4 -4
  219. package/src/components/ui/Select.tsx +56 -0
  220. package/src/components/ui/Toast.tsx +1 -1
  221. package/src/components/ui/index.ts +18 -4
  222. package/src/fields/BlockBuilderField.tsx +3 -3
  223. package/src/fields/DateField.tsx +1 -1
  224. package/src/fields/RelationshipField.tsx +10 -10
  225. package/src/fields/TextField.tsx +1 -1
  226. package/src/index.ts +32 -0
  227. package/src/layout/Header.tsx +28 -28
  228. package/src/layout/Layout.tsx +39 -46
  229. package/src/layout/Sidebar.tsx +37 -64
  230. package/src/layout/primitives/AdminShell.tsx +118 -0
  231. package/src/layout/primitives/Box.tsx +30 -0
  232. package/src/layout/primitives/Cluster.tsx +74 -0
  233. package/src/layout/primitives/Grid.tsx +120 -0
  234. package/src/layout/primitives/PageContainer.tsx +96 -0
  235. package/src/layout/primitives/Split.tsx +73 -0
  236. package/src/layout/primitives/Stack.tsx +67 -0
  237. package/src/layout/primitives/index.ts +36 -0
  238. package/src/layout/primitives/tokens.ts +76 -0
  239. package/src/lib/cv.ts +96 -0
  240. package/src/styles/build-input.css +1 -1
  241. package/src/views/ApiKeys.tsx +57 -57
  242. package/src/views/CollectionList.tsx +30 -30
  243. package/src/views/Dashboard.tsx +737 -186
  244. package/src/views/DocumentEdit.tsx +90 -10
  245. package/src/views/ForgotPassword.tsx +18 -18
  246. package/src/views/FormEditor.tsx +75 -75
  247. package/src/views/FormSubmissions.tsx +76 -76
  248. package/src/views/Forms.tsx +27 -27
  249. package/src/views/Login.tsx +65 -25
  250. package/src/views/MediaBrowser.tsx +127 -127
  251. package/src/views/PageEditor.tsx +25 -25
  252. package/src/views/Pages.tsx +59 -59
  253. package/src/views/PostEditor.tsx +37 -37
  254. package/src/views/Posts.tsx +48 -48
  255. package/src/views/Redirects.tsx +21 -21
  256. package/src/views/ResetPassword.tsx +28 -28
  257. package/src/views/SEO.tsx +144 -144
  258. package/src/views/ScriptTagEditor.tsx +24 -24
  259. package/src/views/ScriptTags.tsx +10 -10
  260. package/src/views/Settings.tsx +88 -80
  261. package/src/views/SetupWizard.tsx +28 -28
  262. package/src/views/Users.tsx +20 -20
  263. package/src/views/page-builder/AIBlockAssist.tsx +1 -1
  264. package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
  265. package/src/views/page-builder/BlockEditor.tsx +26 -26
  266. package/src/views/page-builder/BlockPicker.tsx +22 -22
  267. package/src/views/page-builder/BottomBar.tsx +8 -8
  268. package/src/views/page-builder/BuilderToolbar.tsx +17 -17
  269. package/src/views/page-builder/ContextPanel.tsx +3 -3
  270. package/src/views/page-builder/DesignScore.tsx +21 -21
  271. package/src/views/page-builder/NodeSettings.tsx +27 -27
  272. package/src/views/page-builder/PageBuilder.tsx +11 -11
  273. package/src/views/page-builder/PageSettings.tsx +4 -4
  274. package/src/views/page-builder/PageTemplates.tsx +18 -18
  275. package/src/views/page-builder/SEOPanel.tsx +53 -53
  276. package/src/views/page-builder/SavedSections.tsx +37 -37
  277. package/src/views/page-builder/TemplatePicker.tsx +17 -17
  278. package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
  279. package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
  280. package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
  281. package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
  282. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
  283. package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
  284. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
  285. package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
  286. package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
  287. package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
  288. package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
  289. package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
  290. package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
  291. package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
  292. package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
  293. package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
  294. package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
@@ -120,27 +120,27 @@ export function CollectionList({ collectionSlug, config, onNavigate }) {
120
120
  setSelected(new Set());
121
121
  refetch();
122
122
  };
123
- const SortCol = ({ field, children }) => (_jsxs("button", { type: "button", onClick: () => toggleSort(field), className: "flex items-center gap-1 text-xs font-medium hover:opacity-80 transition-opacity", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [children, sort === field && _jsx("span", { children: order === 'asc' ? '↑' : '↓' })] }));
123
+ const SortCol = ({ field, children }) => (_jsxs("button", { type: "button", onClick: () => toggleSort(field), className: "flex items-center gap-1 text-xs font-medium transition-opacity hover:opacity-80", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [children, sort === field && _jsx("span", { children: order === 'asc' ? '↑' : '↓' })] }));
124
124
  if (loading && docs.length === 0) {
125
- return (_jsx("div", { className: "p-4 flex items-center justify-center h-64", children: _jsx(Loader2, { className: "w-6 h-6 animate-spin", style: { color: 'var(--actuate-primary, #2563eb)' } }) }));
125
+ return (_jsx("div", { className: "flex h-64 items-center justify-center p-4", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin", style: { color: 'var(--actuate-primary, #2563eb)' } }) }));
126
126
  }
127
- return (_jsxs("div", { className: "p-3 pr-6 sm:p-4 sm:pr-8 h-full flex flex-col", children: [_jsxs("div", { className: "flex items-center justify-between mb-4 gap-3 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl sm:text-2xl font-semibold", style: { color: 'var(--actuate-text, #111827)' }, children: labels.plural }), _jsxs("p", { className: "text-sm", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [total, " total"] })] }), _jsxs("button", { onClick: () => onNavigate(`/${collectionSlug}/new`), className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white transition-colors", style: { background: 'var(--actuate-primary, #2563eb)' }, children: [_jsx(Plus, { className: "w-4 h-4" }), " New ", labels.singular] })] }), _jsxs("div", { className: "relative mb-4", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4", style: { color: 'var(--actuate-text-muted, #9ca3af)' } }), _jsx("input", { type: "text", placeholder: `Search ${labels.plural.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), className: "w-full pl-9 pr-3 py-2 text-sm rounded-lg border focus:outline-none focus:ring-2", style: {
127
+ return (_jsxs("div", { className: "flex h-full flex-col p-3 pr-6 sm:p-4 sm:pr-8", children: [_jsxs("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold sm:text-2xl", style: { color: 'var(--actuate-text, #111827)' }, children: labels.plural }), _jsxs("p", { className: "text-sm", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [total, " total"] })] }), _jsxs("button", { onClick: () => onNavigate(`/${collectionSlug}/new`), className: "flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors", style: { background: 'var(--actuate-primary, #2563eb)' }, children: [_jsx(Plus, { className: "h-4 w-4" }), " New ", labels.singular] })] }), _jsxs("div", { className: "relative mb-4", children: [_jsx(Search, { className: "absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2", style: { color: 'var(--actuate-text-muted, #9ca3af)' } }), _jsx("input", { type: "text", placeholder: `Search ${labels.plural.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), className: "w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus:ring-2 focus:outline-none", style: {
128
128
  borderColor: 'var(--actuate-border, #d1d5db)',
129
129
  color: 'var(--actuate-text, #111827)',
130
- } })] }), selected.size > 0 && (_jsxs("div", { className: "rounded-lg p-3 mb-4 flex flex-wrap items-center justify-between gap-2", style: {
130
+ } })] }), selected.size > 0 && (_jsxs("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-2 rounded-lg p-3", style: {
131
131
  background: 'var(--actuate-info-bg, #eff6ff)',
132
132
  borderColor: 'var(--actuate-info-border, #bfdbfe)',
133
133
  borderWidth: 1,
134
134
  borderStyle: 'solid',
135
- }, children: [_jsxs("span", { className: "text-sm", style: { color: 'var(--actuate-info-text, #1e40af)' }, children: [selected.size, " selected"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => bulkAction('publish'), className: "px-3 py-1.5 text-sm text-white rounded-lg", style: { background: 'var(--actuate-success, #16a34a)' }, children: "Publish" }), _jsx("button", { onClick: () => bulkAction('unpublish'), className: "px-3 py-1.5 text-sm text-white rounded-lg", style: { background: 'var(--actuate-warning, #ca8a04)' }, children: "Unpublish" }), _jsxs("button", { onClick: () => bulkAction('delete'), className: "px-3 py-1.5 text-sm text-white rounded-lg", style: { background: 'var(--actuate-danger, #dc2626)' }, children: [_jsx(Trash2, { className: "w-3.5 h-3.5 inline -mt-0.5 mr-1" }), "Delete"] })] })] })), error && (_jsxs("div", { className: "rounded-lg p-3 mb-4 text-sm", style: {
135
+ }, children: [_jsxs("span", { className: "text-sm", style: { color: 'var(--actuate-info-text, #1e40af)' }, children: [selected.size, " selected"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => bulkAction('publish'), className: "rounded-lg px-3 py-1.5 text-sm text-white", style: { background: 'var(--actuate-success, #16a34a)' }, children: "Publish" }), _jsx("button", { onClick: () => bulkAction('unpublish'), className: "rounded-lg px-3 py-1.5 text-sm text-white", style: { background: 'var(--actuate-warning, #ca8a04)' }, children: "Unpublish" }), _jsxs("button", { onClick: () => bulkAction('delete'), className: "rounded-lg px-3 py-1.5 text-sm text-white", style: { background: 'var(--actuate-danger, #dc2626)' }, children: [_jsx(Trash2, { className: "-mt-0.5 mr-1 inline h-3.5 w-3.5" }), "Delete"] })] })] })), error && (_jsxs("div", { className: "mb-4 rounded-lg p-3 text-sm", style: {
136
136
  background: 'var(--actuate-danger-bg, #fef2f2)',
137
137
  color: 'var(--actuate-danger-text, #991b1b)',
138
- }, children: [error, " \u2014", ' ', _jsx("button", { onClick: refetch, className: "underline", children: "retry" })] })), docs.length === 0 && !loading ? (_jsxs("div", { className: "flex-1 flex flex-col items-center justify-center rounded-lg border p-8", style: {
138
+ }, children: [error, " \u2014", ' ', _jsx("button", { onClick: refetch, className: "underline", children: "retry" })] })), docs.length === 0 && !loading ? (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center rounded-lg border p-8", style: {
139
139
  borderColor: 'var(--actuate-border, #d1d5db)',
140
140
  color: 'var(--actuate-text-secondary, #6b7280)',
141
- }, children: [_jsx(FileText, { className: "w-10 h-10 mb-3 opacity-40" }), _jsxs("p", { className: "text-sm mb-3", children: ["No ", labels.plural.toLowerCase(), " found"] }), _jsxs("button", { onClick: () => onNavigate(`/${collectionSlug}/new`), className: "px-4 py-2 text-sm text-white rounded-lg", style: { background: 'var(--actuate-primary, #2563eb)' }, children: ["Create ", labels.singular] })] })) : (_jsx("div", { className: "flex-1 rounded-lg border overflow-auto", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "sticky top-0", style: {
141
+ }, children: [_jsx(FileText, { className: "mb-3 h-10 w-10 opacity-40" }), _jsxs("p", { className: "mb-3 text-sm", children: ["No ", labels.plural.toLowerCase(), " found"] }), _jsxs("button", { onClick: () => onNavigate(`/${collectionSlug}/new`), className: "rounded-lg px-4 py-2 text-sm text-white", style: { background: 'var(--actuate-primary, #2563eb)' }, children: ["Create ", labels.singular] })] })) : (_jsx("div", { className: "flex-1 overflow-auto rounded-lg border", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "sticky top-0", style: {
142
142
  background: 'var(--actuate-surface-alt, #f9fafb)',
143
143
  borderBottom: '1px solid var(--actuate-border, #d1d5db)',
144
- }, children: _jsxs("tr", { children: [_jsx("th", { className: "w-10 px-3 py-2 text-left", children: _jsx("input", { type: "checkbox", checked: selected.size === docs.length && docs.length > 0, onChange: (e) => toggleAll(e.target.checked), className: "rounded" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "title", children: "Title" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "status", children: "Status" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "updatedAt", children: "Updated" }) }), _jsx("th", { className: "px-3 py-2 text-left text-xs font-medium", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: "Actions" })] }) }), _jsx("tbody", { children: docs.map((doc) => (_jsxs("tr", { className: "transition-colors hover:opacity-95", style: { borderBottom: '1px solid var(--actuate-border, #e5e7eb)' }, children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", checked: selected.has(String(doc.id)), onChange: () => toggleSelect(String(doc.id)), className: "rounded" }) }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onNavigate(`/${collectionSlug}/${doc.id}`), className: "font-medium text-left hover:underline", style: { color: 'var(--actuate-text, #111827)' }, children: doc.title || doc.name || `#${doc.id}` }) }), _jsx("td", { className: "px-3 py-2", children: _jsx("span", { className: "inline-block px-2 py-0.5 rounded-full text-xs font-medium", style: { background: statusColor(doc.status), color: statusText(doc.status) }, children: doc.status ?? 'DRAFT' }) }), _jsx("td", { className: "px-3 py-2", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: formatDate(doc.updatedAt) }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onNavigate(`/${collectionSlug}/${doc.id}`), className: "p-1.5 rounded hover:opacity-75 transition-opacity", title: "Edit", children: _jsx(MoreHorizontal, { className: "w-4 h-4", style: { color: 'var(--actuate-text-secondary, #6b7280)' } }) }) })] }, doc.id))) })] }) })), totalPages > 1 && (_jsxs("div", { className: "flex items-center justify-between mt-4 text-sm", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [_jsxs("span", { children: ["Page ", page, " of ", totalPages] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { disabled: page <= 1, onClick: () => setPage((p) => p - 1), className: "flex items-center gap-1 px-3 py-1.5 rounded-lg border disabled:opacity-40 transition-opacity", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: [_jsx(ChevronLeft, { className: "w-4 h-4" }), " Previous"] }), _jsxs("button", { disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), className: "flex items-center gap-1 px-3 py-1.5 rounded-lg border disabled:opacity-40 transition-opacity", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: ["Next ", _jsx(ChevronRight, { className: "w-4 h-4" })] })] })] }))] }));
144
+ }, children: _jsxs("tr", { children: [_jsx("th", { className: "w-10 px-3 py-2 text-left", children: _jsx("input", { type: "checkbox", checked: selected.size === docs.length && docs.length > 0, onChange: (e) => toggleAll(e.target.checked), className: "rounded" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "title", children: "Title" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "status", children: "Status" }) }), _jsx("th", { className: "px-3 py-2 text-left", children: _jsx(SortCol, { field: "updatedAt", children: "Updated" }) }), _jsx("th", { className: "px-3 py-2 text-left text-xs font-medium", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: "Actions" })] }) }), _jsx("tbody", { children: docs.map((doc) => (_jsxs("tr", { className: "transition-colors hover:opacity-95", style: { borderBottom: '1px solid var(--actuate-border, #e5e7eb)' }, children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", checked: selected.has(String(doc.id)), onChange: () => toggleSelect(String(doc.id)), className: "rounded" }) }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onNavigate(`/${collectionSlug}/${doc.id}`), className: "text-left font-medium hover:underline", style: { color: 'var(--actuate-text, #111827)' }, children: doc.title || doc.name || `#${doc.id}` }) }), _jsx("td", { className: "px-3 py-2", children: _jsx("span", { className: "inline-block rounded-full px-2 py-0.5 text-xs font-medium", style: { background: statusColor(doc.status), color: statusText(doc.status) }, children: doc.status ?? 'DRAFT' }) }), _jsx("td", { className: "px-3 py-2", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: formatDate(doc.updatedAt) }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onNavigate(`/${collectionSlug}/${doc.id}`), className: "rounded p-1.5 transition-opacity hover:opacity-75", title: "Edit", children: _jsx(MoreHorizontal, { className: "h-4 w-4", style: { color: 'var(--actuate-text-secondary, #6b7280)' } }) }) })] }, doc.id))) })] }) })), totalPages > 1 && (_jsxs("div", { className: "mt-4 flex items-center justify-between text-sm", style: { color: 'var(--actuate-text-secondary, #6b7280)' }, children: [_jsxs("span", { children: ["Page ", page, " of ", totalPages] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { disabled: page <= 1, onClick: () => setPage((p) => p - 1), className: "flex items-center gap-1 rounded-lg border px-3 py-1.5 transition-opacity disabled:opacity-40", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), " Previous"] }), _jsxs("button", { disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), className: "flex items-center gap-1 rounded-lg border px-3 py-1.5 transition-opacity disabled:opacity-40", style: { borderColor: 'var(--actuate-border, #d1d5db)' }, children: ["Next ", _jsx(ChevronRight, { className: "h-4 w-4" })] })] })] }))] }));
145
145
  }
146
146
  //# sourceMappingURL=CollectionList.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/views/Dashboard.tsx"],"names":[],"mappings":"AAoDA,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,OAAO,CAAC,EAAE,GAAG,CAAA;IACb,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAoED,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,cAAc,2CA6MxE"}
1
+ {"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/views/Dashboard.tsx"],"names":[],"mappings":"AAwEA,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,OAAO,CAAC,EAAE,GAAG,CAAA;IACb,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AA0HD,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,cAAc,2CAinBxE"}
@@ -1,124 +1,379 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { FileText, File, Image, ClipboardList, Search, Loader2, AlertTriangle, Database, ChevronLeft, ChevronRight, } from 'lucide-react';
4
- import { useState } from 'react';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Dashboard
5
+ * ----------
6
+ * Single-file, lean dashboard implementation. Two API calls only
7
+ * (`/stats` + `/health`, both shared with the rest of the admin), everything
8
+ * else derived client-side via `useMemo`. No charting libraries on this view
9
+ * (the design uses plain progress bars + colored dots), keeping the bundle
10
+ * and TTI cost low.
11
+ *
12
+ * Responsive layout:
13
+ * < 640px (mobile): stat cards 2-col, main grid stacks, quick actions scroll-x
14
+ * 640-1023 (tablet): stat cards 3-col, main grid stacks
15
+ * >= 1024 (desktop): stat cards 5-col, main grid is `1fr 320px`
16
+ */
17
+ import { useMemo, useState } from 'react';
18
+ import { FileText, File as FileIcon, Image as ImageIcon, ClipboardList, Search, Plus, Upload, Globe, ExternalLink, Activity, AlertTriangle, Clock, Zap, Database, ChevronRight, Loader2, } from 'lucide-react';
5
19
  import { useApiData } from '../lib/useApiData.js';
6
- import { ContentOverviewChart } from '../components/ContentOverviewChart.js';
7
- import { SEOPerformance } from '../components/SEOPerformance.js';
20
+ // ─── helpers (kept top-level so they aren't re-created on every render) ──────
8
21
  function resolveCollections(config) {
9
22
  if (!config?.collections)
10
23
  return [];
11
24
  const raw = config.collections;
12
25
  const list = Array.isArray(raw) ? raw : Object.values(raw);
13
26
  return list
14
- .filter((c) => !c.admin?.hidden)
27
+ .filter((c) => !c?.admin?.hidden)
15
28
  .map((c) => ({ slug: c.slug, type: c.type, labels: c.labels }));
16
29
  }
17
30
  function collectionLabel(col, plural = true) {
31
+ const fallback = col.slug.charAt(0).toUpperCase() + col.slug.slice(1);
18
32
  if (plural)
19
- return col.labels?.plural ?? col.slug.charAt(0).toUpperCase() + col.slug.slice(1);
20
- return col.labels?.singular ?? col.slug.charAt(0).toUpperCase() + col.slug.slice(1);
33
+ return col.labels?.plural ?? fallback;
34
+ return col.labels?.singular ?? fallback;
21
35
  }
22
36
  function relativeTime(dateStr) {
23
- const now = Date.now();
24
37
  const then = new Date(dateStr).getTime();
25
- const diff = now - then;
26
- const mins = Math.floor(diff / 60000);
27
- if (mins < 1)
38
+ const diff = Date.now() - then;
39
+ if (diff < 60_000)
28
40
  return 'just now';
41
+ const mins = Math.floor(diff / 60_000);
29
42
  if (mins < 60)
30
- return `${mins} min ago`;
43
+ return `${mins}m ago`;
31
44
  const hours = Math.floor(mins / 60);
32
45
  if (hours < 24)
33
- return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
46
+ return `${hours}h ago`;
34
47
  const days = Math.floor(hours / 24);
48
+ if (days === 1)
49
+ return 'Yesterday';
35
50
  if (days < 30)
36
- return `${days} day${days !== 1 ? 's' : ''} ago`;
51
+ return `${days}d ago`;
37
52
  return new Date(dateStr).toLocaleDateString();
38
53
  }
39
- function statusColor(status) {
40
- switch (status) {
41
- case 'PUBLISHED':
42
- return 'bg-green-100 text-green-800';
43
- case 'DRAFT':
44
- return 'bg-gray-100 text-gray-700';
45
- case 'SCHEDULED':
46
- return 'bg-purple-100 text-purple-800';
47
- case 'IN_REVIEW':
48
- return 'bg-blue-100 text-blue-800';
49
- default:
50
- return 'bg-gray-100 text-gray-700';
51
- }
54
+ function timeOfDayGreeting() {
55
+ const h = new Date().getHours();
56
+ if (h < 12)
57
+ return 'Good morning';
58
+ if (h < 17)
59
+ return 'Good afternoon';
60
+ return 'Good evening';
52
61
  }
53
- function statusLabel(status) {
62
+ function todayDateString() {
63
+ return new Date().toLocaleDateString('en-US', {
64
+ weekday: 'long',
65
+ month: 'long',
66
+ day: 'numeric',
67
+ year: 'numeric',
68
+ });
69
+ }
70
+ // Deterministic colour per author so avatars stay stable across renders
71
+ // without storing anything in state or hitting the server.
72
+ const AVATAR_COLORS = [
73
+ '#7C3AED', // purple
74
+ '#E11D48', // rose
75
+ '#059669', // emerald
76
+ '#0891B2', // cyan
77
+ '#D97706', // amber
78
+ '#4F46E5', // indigo
79
+ '#DC2626', // red
80
+ ];
81
+ function hashString(s) {
82
+ let h = 0;
83
+ for (let i = 0; i < s.length; i++)
84
+ h = (h * 31 + s.charCodeAt(i)) | 0;
85
+ return Math.abs(h);
86
+ }
87
+ function authorAvatar(name) {
88
+ const safe = (name ?? '').trim() || 'User';
89
+ const parts = safe.split(/\s+/).filter(Boolean);
90
+ const initials = parts.length >= 2
91
+ ? `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase()
92
+ : safe.slice(0, 2).toUpperCase();
93
+ const color = AVATAR_COLORS[hashString(safe) % AVATAR_COLORS.length];
94
+ return { color, initials };
95
+ }
96
+ function statusBadge(status) {
54
97
  switch (status) {
55
98
  case 'PUBLISHED':
56
- return 'Published';
99
+ return {
100
+ label: 'Published',
101
+ cls: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300',
102
+ };
57
103
  case 'DRAFT':
58
- return 'Draft';
104
+ return {
105
+ label: 'Draft',
106
+ cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
107
+ };
59
108
  case 'SCHEDULED':
60
- return 'Scheduled';
109
+ return {
110
+ label: 'Scheduled',
111
+ cls: 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300',
112
+ };
61
113
  case 'IN_REVIEW':
62
- return 'In Review';
114
+ return {
115
+ label: 'In Review',
116
+ cls: 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300',
117
+ };
63
118
  default:
64
- return status;
119
+ return { label: status, cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' };
65
120
  }
66
121
  }
67
- const STAT_COLORS = [
68
- { bg: 'bg-blue-50', icon: 'bg-blue-100', text: 'text-blue-600' },
69
- { bg: 'bg-purple-50', icon: 'bg-purple-100', text: 'text-purple-600' },
70
- { bg: 'bg-teal-50', icon: 'bg-teal-100', text: 'text-teal-600' },
71
- { bg: 'bg-green-50', icon: 'bg-green-100', text: 'text-green-600' },
72
- { bg: 'bg-amber-50', icon: 'bg-amber-100', text: 'text-amber-600' },
73
- ];
122
+ // ─── Dashboard ───────────────────────────────────────────────────────────────
74
123
  export function Dashboard({ config, session, onNavigate }) {
75
124
  const nav = (path) => onNavigate?.(path);
76
- const { data, loading, error, exhausted, refetch } = useApiData('/stats');
125
+ const { data: stats, loading, error, exhausted, refetch } = useApiData('/stats');
77
126
  const { data: health } = useApiData('/health');
78
- const [activityPage, setActivityPage] = useState(0);
79
- const collections = resolveCollections(config);
80
- const userName = session?.name ?? session?.email?.split('@')[0] ?? 'Admin';
81
- const collectionCounts = data?.collectionCounts ?? {};
82
- const statusCounts = data?.statusCounts ?? {};
83
- const recentDocs = data?.recentDocuments ?? [];
84
- const perPage = 5;
85
- const totalActivityPages = Math.max(1, Math.ceil(recentDocs.length / perPage));
86
- const visibleDocs = recentDocs.slice(activityPage * perPage, (activityPage + 1) * perPage);
87
- const statCards = [];
88
- if (collections.length > 0) {
89
- for (const col of collections.slice(0, 2)) {
90
- statCards.push({
91
- label: collectionLabel(col),
92
- value: collectionCounts[col.slug] ?? 0,
93
- icon: col.type === 'page' ? File : FileText,
127
+ const greeting = useMemo(() => timeOfDayGreeting(), []);
128
+ const dateStr = useMemo(() => todayDateString(), []);
129
+ const userName = session?.name ?? session?.email?.split('@')[0] ?? 'there';
130
+ const collections = useMemo(() => resolveCollections(config), [config]);
131
+ // ── Stat cards ──────────────────────────────────────────────────────────
132
+ const statCards = useMemo(() => {
133
+ const counts = stats?.collectionCounts ?? {};
134
+ // Prefer two real collections (the user's primary content types) over
135
+ // hard-coded Posts/Pages so the dashboard adapts to admin-managed types.
136
+ const primary = [];
137
+ const posts = collections.find((c) => c.slug === 'posts' || c.type === 'post');
138
+ const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page');
139
+ if (posts)
140
+ primary.push(posts);
141
+ if (pages)
142
+ primary.push(pages);
143
+ if (primary.length < 2) {
144
+ for (const c of collections) {
145
+ if (primary.length >= 2)
146
+ break;
147
+ if (!primary.includes(c))
148
+ primary.push(c);
149
+ }
150
+ }
151
+ const cards = [];
152
+ if (primary[0]) {
153
+ cards.push({
154
+ label: collectionLabel(primary[0]),
155
+ value: String(counts[primary[0].slug] ?? 0),
156
+ icon: primary[0].type === 'page' ? FileIcon : FileText,
157
+ iconBg: 'bg-violet-100 dark:bg-violet-950/60',
158
+ iconColor: 'text-violet-600 dark:text-violet-300',
159
+ href: `/${primary[0].slug}`,
94
160
  });
95
161
  }
162
+ else {
163
+ cards.push({
164
+ label: 'Documents',
165
+ value: String(stats?.totalDocuments ?? 0),
166
+ icon: FileText,
167
+ iconBg: 'bg-violet-100 dark:bg-violet-950/60',
168
+ iconColor: 'text-violet-600 dark:text-violet-300',
169
+ });
170
+ }
171
+ if (primary[1]) {
172
+ cards.push({
173
+ label: collectionLabel(primary[1]),
174
+ value: String(counts[primary[1].slug] ?? 0),
175
+ icon: primary[1].type === 'page' ? FileIcon : FileText,
176
+ iconBg: 'bg-cyan-100 dark:bg-cyan-950/60',
177
+ iconColor: 'text-cyan-600 dark:text-cyan-300',
178
+ href: `/${primary[1].slug}`,
179
+ });
180
+ }
181
+ cards.push({
182
+ label: 'Media',
183
+ value: String(stats?.totalMedia ?? 0),
184
+ hint: stats && stats.totalMedia > 0
185
+ ? `${stats.totalMedia} file${stats.totalMedia === 1 ? '' : 's'}`
186
+ : undefined,
187
+ icon: ImageIcon,
188
+ iconBg: 'bg-emerald-100 dark:bg-emerald-950/60',
189
+ iconColor: 'text-emerald-600 dark:text-emerald-300',
190
+ href: '/media',
191
+ });
192
+ // `formCount` in /stats is the *submission* count; the number of
193
+ // forms is the count of `collection: forms` documents.
194
+ const formsCount = counts['forms'] ?? 0;
195
+ const submissions = stats?.formCount ?? 0;
196
+ cards.push({
197
+ label: 'Forms',
198
+ value: String(formsCount),
199
+ hint: submissions > 0 ? `${submissions} response${submissions === 1 ? '' : 's'}` : undefined,
200
+ icon: ClipboardList,
201
+ iconBg: 'bg-amber-100 dark:bg-amber-950/60',
202
+ iconColor: 'text-amber-600 dark:text-amber-300',
203
+ href: '/forms',
204
+ });
205
+ const seo = stats?.avgSeoScore ?? 0;
206
+ cards.push({
207
+ label: 'SEO Score',
208
+ value: seo > 0 ? String(seo) : '—',
209
+ unit: seo > 0 ? '/100' : undefined,
210
+ hint: seo > 0 ? (seo >= 70 ? 'Good' : seo >= 40 ? 'Fair' : 'Needs work') : 'No content yet',
211
+ hintUp: seo >= 70,
212
+ icon: Search,
213
+ iconBg: 'bg-indigo-100 dark:bg-indigo-950/60',
214
+ iconColor: 'text-indigo-600 dark:text-indigo-300',
215
+ href: '/seo',
216
+ });
217
+ return cards;
218
+ }, [stats, collections]);
219
+ // ── Quick actions ───────────────────────────────────────────────────────
220
+ // "New Post" lives only in the hero CTA above; this row is the secondary
221
+ // surface and intentionally omits the primary action to avoid the obvious
222
+ // duplication. Order mirrors the natural authoring flow.
223
+ const quickActions = useMemo(() => {
224
+ const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page');
225
+ const items = [];
226
+ if (pages)
227
+ items.push({ label: 'New Page', icon: Plus, onClick: () => nav(`/${pages.slug}/new`) });
228
+ items.push({ label: 'Upload Media', icon: Upload, onClick: () => nav('/media') });
229
+ items.push({ label: 'New Form', icon: Plus, onClick: () => nav('/forms') });
230
+ items.push({ label: 'Manage SEO', icon: Search, onClick: () => nav('/seo') });
231
+ items.push({ label: 'View API', icon: Globe, onClick: () => nav('/api-keys') });
232
+ return items;
233
+ }, [collections, onNavigate]); // eslint-disable-line react-hooks/exhaustive-deps
234
+ // ── Recent activity ─────────────────────────────────────────────────────
235
+ const [activityLimit, setActivityLimit] = useState(8);
236
+ const activity = useMemo(() => {
237
+ const docs = stats?.recentDocuments ?? [];
238
+ return docs.slice(0, activityLimit).map((d) => {
239
+ const col = collections.find((c) => c.slug === d.collection);
240
+ return {
241
+ ...d,
242
+ typeLabel: col ? collectionLabel(col, false) : d.collection,
243
+ relTime: relativeTime(d.updatedAt),
244
+ avatar: authorAvatar(d.author),
245
+ statusInfo: statusBadge(d.status),
246
+ };
247
+ });
248
+ }, [stats, collections, activityLimit]);
249
+ // ── Publishing queue (scheduled docs only) ──────────────────────────────
250
+ const publishQueue = useMemo(() => {
251
+ const docs = stats?.recentDocuments ?? [];
252
+ return docs
253
+ .filter((d) => d.status === 'SCHEDULED')
254
+ .slice(0, 5)
255
+ .map((d) => {
256
+ const col = collections.find((c) => c.slug === d.collection);
257
+ return {
258
+ id: d.id,
259
+ collection: d.collection,
260
+ title: d.title || 'Untitled',
261
+ type: col ? collectionLabel(col, false) : d.collection,
262
+ date: relativeTime(d.updatedAt),
263
+ author: d.author,
264
+ };
265
+ });
266
+ }, [stats, collections]);
267
+ // ── Content health summary (derived from /stats) ────────────────────────
268
+ const contentHealth = useMemo(() => {
269
+ const score = stats?.avgSeoScore ?? 0;
270
+ const counts = stats?.statusCounts ?? {};
271
+ const totalDrafts = counts['DRAFT'] ?? 0;
272
+ const totalScheduled = counts['SCHEDULED'] ?? 0;
273
+ const totalInReview = counts['IN_REVIEW'] ?? 0;
274
+ const issues = [];
275
+ if (totalDrafts > 0) {
276
+ issues.push({ label: 'Drafts pending publish', count: totalDrafts, tone: 'warn' });
277
+ }
278
+ if (totalInReview > 0) {
279
+ issues.push({ label: 'Awaiting review', count: totalInReview, tone: 'warn' });
280
+ }
281
+ if (totalScheduled > 0) {
282
+ issues.push({ label: 'Scheduled to publish', count: totalScheduled, tone: 'muted' });
283
+ }
284
+ if (score > 0 && score < 70) {
285
+ issues.push({
286
+ label: 'Pages with weak SEO',
287
+ count: Math.max(1, Math.round((70 - score) / 10)),
288
+ tone: score < 40 ? 'err' : 'warn',
289
+ });
290
+ }
291
+ return {
292
+ score,
293
+ label: score >= 70 ? 'Good' : score >= 40 ? 'Fair' : score > 0 ? 'Poor' : 'No data',
294
+ tone: score >= 70 ? 'ok' : score >= 40 ? 'warn' : score > 0 ? 'err' : 'muted',
295
+ issues,
296
+ };
297
+ }, [stats]);
298
+ // ── Content delivery tiles ──────────────────────────────────────────────
299
+ const delivery = useMemo(() => {
300
+ const sched = stats?.statusCounts?.['SCHEDULED'] ?? 0;
301
+ return {
302
+ totalDocs: stats?.totalDocuments ?? 0,
303
+ forms: stats?.formCount ?? 0,
304
+ scheduled: sched,
305
+ webhooks: {
306
+ total: stats?.webhookCount ?? 0,
307
+ active: stats?.webhookActiveCount ?? 0,
308
+ },
309
+ };
310
+ }, [stats]);
311
+ const totalIssues = contentHealth.issues.reduce((s, i) => s + i.count, 0);
312
+ // ── Render ──────────────────────────────────────────────────────────────
313
+ if (loading && !stats) {
314
+ return (_jsx("div", { className: "flex h-64 items-center justify-center p-4 sm:p-6", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-violet-600" }) }));
96
315
  }
97
- else {
98
- statCards.push({ label: 'Pages', value: collectionCounts['pages'] ?? 0, icon: File });
99
- statCards.push({ label: 'Posts', value: collectionCounts['posts'] ?? 0, icon: FileText });
100
- }
101
- statCards.push({ label: 'Forms', value: data?.formCount ?? 0, icon: ClipboardList });
102
- statCards.push({ label: 'Media', value: data?.totalMedia ?? 0, icon: Image });
103
- statCards.push({ label: 'Avg. SEO Rating', value: data?.avgSeoScore ?? 0, icon: Search });
104
- if (loading) {
105
- return (_jsx("div", { className: "p-4 sm:p-6 flex items-center justify-center h-64", children: _jsx(Loader2, { className: "w-6 h-6 animate-spin text-blue-600" }) }));
106
- }
107
- return (_jsxs("div", { className: "p-4 sm:p-6 space-y-6", children: [health && health.status === 'degraded' && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-blue-200 bg-blue-50 p-3", children: [_jsx(Database, { className: "w-5 h-5 text-blue-600 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-sm font-medium text-blue-900", children: "Database Setup Required" }), _jsx("p", { className: "text-xs text-blue-700 mt-0.5", children: !health.databaseConnected
316
+ const heroPostSlug = collections.find((c) => c.slug === 'posts' || c.type === 'post')?.slug ?? 'posts';
317
+ const siteUrl = config?.site?.url ?? config?.seo?.siteUrl ?? null;
318
+ return (_jsxs("div", { className: "w-full space-y-5 p-4 sm:p-6 lg:px-8", children: [health && health.status === 'degraded' && (_jsxs("div", { className: "flex items-start gap-3 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-900 dark:bg-blue-950/40", children: [_jsx(Database, { className: "mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "text-sm font-medium text-blue-900 dark:text-blue-200", children: "Database setup required" }), _jsx("p", { className: "mt-0.5 text-xs text-blue-700 dark:text-blue-300", children: !health.databaseConnected
108
319
  ? 'Cannot connect to the database. Check your DATABASE_URL environment variable.'
109
320
  : !health.secretConfigured
110
321
  ? 'CMS secret not configured. Set CMS_SECRET or CMS_SESSION_SECRET (min 32 characters).'
111
322
  : `Some CMS models are missing: ${Object.entries(health.models)
112
323
  .filter(([, v]) => !v)
113
324
  .map(([k]) => k)
114
- .join(', ')}. Run your database migrations.` })] })] })), error && exhausted && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx(AlertTriangle, { className: "w-5 h-5 text-amber-600 shrink-0" }), _jsx("span", { className: "text-sm text-amber-800 flex-1", children: "Some dashboard data may be unavailable." }), _jsx("button", { onClick: refetch, className: "px-3 py-1 text-sm text-amber-700 border border-amber-300 rounded-lg hover:bg-amber-100 transition-colors", children: "Retry" })] })), _jsxs("div", { children: [_jsxs("h1", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: ["Welcome back, ", userName] }), _jsx("p", { className: "text-sm text-gray-500 mt-0.5", children: "Here's what's happening with your content today" })] }), _jsx("div", { className: "grid grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4", children: statCards.map((card, i) => {
115
- const colors = STAT_COLORS[i % STAT_COLORS.length];
116
- const Icon = card.icon;
117
- return (_jsxs("div", { className: "bg-white rounded-xl border border-gray-200 p-4", children: [_jsx("div", { className: "flex items-center justify-between mb-3", children: _jsx("div", { className: `w-9 h-9 rounded-lg flex items-center justify-center ${colors.icon}`, children: _jsx(Icon, { className: `w-4.5 h-4.5 ${colors.text}` }) }) }), _jsx("p", { className: "text-2xl font-semibold text-gray-900", children: card.value.toLocaleString() }), _jsx("p", { className: "text-xs text-gray-500 mt-0.5", children: card.label })] }, card.label));
118
- }) }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-4", children: [_jsxs("div", { className: "lg:col-span-8 bg-white rounded-xl border border-gray-200", children: [_jsx("div", { className: "p-4 border-b border-gray-200", children: _jsx("h2", { className: "text-sm font-semibold text-gray-900", children: "Recent Activity" }) }), _jsx("div", { className: "divide-y divide-gray-100", children: visibleDocs.length === 0 ? (_jsx("div", { className: "p-8 text-center", children: _jsx("p", { className: "text-sm text-gray-400", children: "No content yet" }) })) : (visibleDocs.map((doc) => {
119
- const colMeta = collections.find((c) => c.slug === doc.collection);
120
- const typeLabel = colMeta ? collectionLabel(colMeta, false) : doc.collection;
121
- return (_jsx("div", { className: "px-4 py-3 hover:bg-gray-50 transition-colors", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "flex items-center gap-2 mb-0.5", children: _jsx("h3", { className: "text-sm font-medium text-gray-900 truncate", children: doc.title ?? 'Untitled' }) }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-gray-500", children: [_jsx("span", { className: "capitalize", children: typeLabel }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: doc.author }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: relativeTime(doc.updatedAt) })] })] }), _jsx("span", { className: `px-2.5 py-0.5 rounded-full text-xs font-medium whitespace-nowrap ${statusColor(doc.status)}`, children: statusLabel(doc.status) })] }) }, doc.id));
122
- })) }), recentDocs.length > perPage && (_jsxs("div", { className: "px-4 py-3 border-t border-gray-100 flex items-center justify-between text-xs text-gray-500", children: [_jsxs("span", { children: ["Showing ", activityPage * perPage + 1, "-", Math.min((activityPage + 1) * perPage, recentDocs.length), " of ", recentDocs.length] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { onClick: () => setActivityPage(Math.max(0, activityPage - 1)), disabled: activityPage === 0, className: "p-1 rounded hover:bg-gray-100 disabled:opacity-30", children: _jsx(ChevronLeft, { className: "w-3.5 h-3.5" }) }), _jsxs("span", { children: ["Page ", activityPage + 1, " of ", totalActivityPages] }), _jsx("button", { onClick: () => setActivityPage(Math.min(totalActivityPages - 1, activityPage + 1)), disabled: activityPage >= totalActivityPages - 1, className: "p-1 rounded hover:bg-gray-100 disabled:opacity-30", children: _jsx(ChevronRight, { className: "w-3.5 h-3.5" }) })] })] }))] }), _jsxs("div", { className: "lg:col-span-4 bg-white rounded-xl border border-gray-200", children: [_jsx("div", { className: "p-4 border-b border-gray-200", children: _jsx("h2", { className: "text-sm font-semibold text-gray-900", children: "Content Overview" }) }), _jsx("div", { className: "p-4 flex items-center justify-center min-h-[220px]", children: _jsx(ContentOverviewChart, { published: statusCounts['PUBLISHED'] ?? 0, drafts: statusCounts['DRAFT'] ?? 0, scheduled: statusCounts['SCHEDULED'] ?? 0 }) })] })] }), _jsx(SEOPerformance, { onNavigate: nav })] }));
325
+ .join(', ')}. Run your database migrations.` })] })] })), error && exhausted && (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950/40", children: [_jsx(AlertTriangle, { className: "h-5 w-5 shrink-0 text-amber-600 dark:text-amber-400" }), _jsx("span", { className: "flex-1 text-sm text-amber-800 dark:text-amber-200", children: "Some dashboard data may be unavailable." }), _jsx("button", { onClick: refetch, className: "rounded-lg border border-amber-300 px-3 py-1 text-sm text-amber-700 transition-colors hover:bg-amber-100 dark:border-amber-700 dark:text-amber-200 dark:hover:bg-amber-900/40", children: "Retry" })] })), _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("h1", { className: "text-foreground text-xl font-semibold tracking-tight sm:text-2xl", children: [greeting, ", ", userName, " ", _jsx("span", { "aria-hidden": true, children: "\uD83D\uDC4B" })] }), _jsxs("p", { className: "text-muted-foreground mt-0.5 text-sm", children: [dateStr, totalIssues > 0 && (_jsxs("span", { className: "hidden sm:inline", children: [' · ', totalIssues, " content item", totalIssues === 1 ? '' : 's', " need attention"] }))] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsxs("button", { onClick: () => nav(`/${heroPostSlug}/new`), className: "inline-flex items-center gap-1.5 rounded-lg bg-violet-600 px-3.5 py-2 text-sm font-medium text-white transition-colors hover:bg-violet-700", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " New Post"] }), siteUrl && (_jsxs("a", { href: siteUrl, target: "_blank", rel: "noopener noreferrer", className: "border-border bg-card hover:bg-accent hover:text-accent-foreground hidden items-center gap-1.5 rounded-lg border px-3 py-2 text-sm transition-colors sm:inline-flex", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), " View Site"] }))] })] }), _jsx("div", { className: "-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 [scrollbar-width:thin]", children: quickActions.map((a) => (_jsxs("button", { onClick: a.onClick, className: "border-border bg-card inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-3.5 py-2 text-sm shadow-sm transition-colors hover:border-violet-400 hover:bg-violet-50 hover:text-violet-700 dark:hover:bg-violet-950/40 dark:hover:text-violet-300", children: [_jsx(a.icon, { className: "h-3.5 w-3.5" }), a.label] }, a.label))) }), _jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5", children: statCards.map((card) => {
326
+ const Inner = (_jsxs(_Fragment, { children: [_jsx("div", { className: `mb-3 flex h-8 w-8 items-center justify-center rounded-lg ${card.iconBg}`, children: _jsx(card.icon, { className: `h-4 w-4 ${card.iconColor}` }) }), _jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx("span", { className: "text-foreground text-2xl leading-none font-semibold tracking-tight", children: card.value }), card.unit && (_jsx("span", { className: "text-muted-foreground text-sm font-medium", children: card.unit }))] }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: card.label }), card.hint && (_jsx("p", { className: `mt-1.5 text-[11px] ${card.hintUp ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'}`, children: card.hint }))] }));
327
+ return card.href ? (_jsx("button", { type: "button", onClick: () => nav(card.href), className: "bg-card border-border rounded-xl border p-4 text-left shadow-sm transition-shadow hover:shadow-md", children: Inner }, card.label)) : (_jsx("div", { className: "bg-card border-border rounded-xl border p-4 shadow-sm", children: Inner }, card.label));
328
+ }) }), _jsxs("div", { className: "grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Recent Activity" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: "Last 7 days across all content" })] }), _jsx("button", { className: "text-xs font-medium text-violet-600 hover:underline dark:text-violet-400", onClick: () => setActivityLimit((n) => (n >= 20 ? 8 : 20)), children: activity.length >= 20 ? 'Show less' : 'View all' })] }), activity.length === 0 ? (_jsx(EmptyState, { icon: Activity, title: "No activity yet", subtitle: "Content changes will appear here as your team works." })) : (_jsx("ul", { role: "list", className: "divide-border divide-y", children: activity.map((it) => (_jsx("li", { className: "hover:bg-accent/50 cursor-pointer px-4 py-3 transition-colors", onClick: () => nav(`/${it.collection}/${it.id}`), children: _jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [_jsx("div", { className: "mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white", style: { background: it.avatar.color }, "aria-hidden": true, children: it.avatar.initials }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "text-foreground truncate text-sm leading-snug", children: [_jsxs("span", { className: "font-semibold", children: ["\u201C", it.title || 'Untitled', "\u201D"] }), ' ', _jsxs("span", { className: "text-muted-foreground", children: ["\u2014 ", it.typeLabel] })] }), _jsxs("div", { className: "text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]", children: [_jsx("span", { className: `inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`, children: it.statusInfo.label }), _jsx("span", { className: "max-w-[120px] truncate", children: it.author }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: it.relTime })] })] })] }) }, it.id))) }))] }), _jsxs("aside", { className: "flex min-w-0 flex-col gap-4", children: [_jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Publishing Queue" }), _jsxs("p", { className: "text-muted-foreground mt-0.5 text-xs", children: [publishQueue.length, " item", publishQueue.length === 1 ? '' : 's', " scheduled"] })] }), _jsx("button", { className: "text-xs font-medium text-violet-600 hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50 dark:text-violet-400", disabled: publishQueue.length === 0, onClick: () => {
329
+ // We don't have a dedicated "scheduled" admin page, so deep-
330
+ // link to the first scheduled item — that's where authors
331
+ // typically need to land to reschedule or cancel.
332
+ const first = publishQueue[0];
333
+ if (first)
334
+ nav(`/${first.collection}/${first.id}`);
335
+ }, children: "Manage" })] }), publishQueue.length === 0 ? (_jsx(EmptyState, { icon: Clock, title: "No scheduled content", subtitle: "Scheduled posts and pages appear here.", compact: true })) : (_jsx("ul", { role: "list", className: "divide-border divide-y", children: publishQueue.map((q) => (_jsxs("li", { className: "hover:bg-accent/50 flex min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 transition-colors", onClick: () => nav(`/${q.collection}/${q.id}`), children: [_jsx("span", { className: "h-2 w-2 shrink-0 rounded-full bg-amber-500", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground truncate text-sm font-medium", children: q.title }), _jsxs("p", { className: "text-muted-foreground truncate text-[11px]", children: [q.date, " \u00B7 ", q.author] })] }), _jsx("span", { className: "border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize", children: q.type })] }, q.id))) }))] }), _jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Content Health" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: "SEO & quality issues" })] }), _jsx("button", { className: "text-xs font-medium text-violet-600 hover:underline dark:text-violet-400", onClick: () => nav('/seo'), children: "Fix issues" })] }), _jsxs("div", { className: "px-4 pt-3.5 pb-2.5", children: [_jsxs("div", { className: "mb-1.5 flex items-baseline justify-between", children: [_jsxs("p", { className: "text-foreground text-2xl leading-none font-semibold tracking-tight", children: [contentHealth.score > 0 ? contentHealth.score : '—', contentHealth.score > 0 && (_jsx("span", { className: "text-muted-foreground ml-0.5 text-sm font-normal", children: "/100" }))] }), _jsx("span", { className: `text-xs font-medium ${contentHealth.tone === 'ok'
336
+ ? 'text-emerald-600 dark:text-emerald-400'
337
+ : contentHealth.tone === 'warn'
338
+ ? 'text-amber-600 dark:text-amber-400'
339
+ : contentHealth.tone === 'err'
340
+ ? 'text-red-600 dark:text-red-400'
341
+ : 'text-muted-foreground'}`, children: contentHealth.label })] }), _jsx("div", { className: "bg-muted h-1.5 overflow-hidden rounded-full", children: _jsx("div", { className: `h-full rounded-full transition-all ${contentHealth.tone === 'ok'
342
+ ? 'bg-emerald-500'
343
+ : contentHealth.tone === 'warn'
344
+ ? 'bg-amber-500'
345
+ : contentHealth.tone === 'err'
346
+ ? 'bg-red-500'
347
+ : 'bg-muted-foreground/40'}`, style: { width: `${Math.max(0, Math.min(100, contentHealth.score))}%` } }) })] }), contentHealth.issues.length === 0 ? (_jsx("div", { className: "text-muted-foreground px-4 pt-1 pb-4 text-xs", children: "No outstanding issues. Nice work." })) : (_jsx("ul", { role: "list", className: "divide-border divide-y", children: contentHealth.issues.map((iss, i) => (_jsxs("li", { className: "hover:bg-accent/50 flex cursor-pointer items-center gap-2.5 px-4 py-2 transition-colors", onClick: () => nav('/seo'), children: [_jsx("span", { className: `h-1.5 w-1.5 shrink-0 rounded-full ${iss.tone === 'err'
348
+ ? 'bg-red-500'
349
+ : iss.tone === 'warn'
350
+ ? 'bg-amber-500'
351
+ : 'bg-muted-foreground'}`, "aria-hidden": true }), _jsx("span", { className: "text-muted-foreground flex-1 truncate text-xs", children: iss.label }), _jsx("span", { className: `shrink-0 text-sm font-semibold ${iss.tone === 'err'
352
+ ? 'text-red-600 dark:text-red-400'
353
+ : iss.tone === 'warn'
354
+ ? 'text-amber-600 dark:text-amber-400'
355
+ : 'text-muted-foreground'}`, children: iss.count }), _jsx(ChevronRight, { className: "text-muted-foreground/60 h-3.5 w-3.5 shrink-0" })] }, i))) }))] })] })] }), _jsxs("section", { className: "bg-card border-border overflow-hidden rounded-xl border shadow-sm", children: [_jsxs("header", { className: "border-border flex items-center justify-between border-b px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-foreground text-sm font-semibold", children: "Content Delivery" }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: "Real-time platform activity" })] }), _jsxs("button", { className: "inline-flex items-center gap-1 text-xs font-medium text-violet-600 hover:underline dark:text-violet-400", onClick: () => nav('/api-keys'), children: ["API docs ", _jsx(ExternalLink, { className: "h-3 w-3" })] })] }), _jsxs("div", { className: "divide-border grid grid-cols-2 divide-x divide-y lg:grid-cols-4 lg:divide-y-0", children: [_jsx(DeliveryTile, { icon: Activity, label: "Total Documents", value: delivery.totalDocs.toLocaleString(), sub: "across all collections" }), _jsx(DeliveryTile, { icon: Clock, label: "Scheduled Posts", value: delivery.scheduled.toLocaleString(), sub: delivery.scheduled > 0 ? 'in publishing queue' : 'none queued', tone: delivery.scheduled > 0 ? 'ok' : 'muted' }), _jsx(DeliveryTile, { icon: ClipboardList, label: "Form Responses", value: delivery.forms.toLocaleString(), sub: delivery.forms > 0 ? 'total received' : 'none yet', tone: delivery.forms > 0 ? 'ok' : 'muted' }), _jsx(DeliveryTile, { icon: Zap, label: "Active Webhooks", value: `${delivery.webhooks.active} / ${delivery.webhooks.total || 0}`, sub: delivery.webhooks.total === 0
356
+ ? 'none configured'
357
+ : delivery.webhooks.active === delivery.webhooks.total
358
+ ? 'all active'
359
+ : `${delivery.webhooks.total - delivery.webhooks.active} paused`, tone: delivery.webhooks.total === 0
360
+ ? 'muted'
361
+ : delivery.webhooks.active === delivery.webhooks.total
362
+ ? 'ok'
363
+ : 'warn' })] })] })] }));
364
+ }
365
+ // ─── Sub-components (kept in-file: smaller bundle, no extra module hops) ────
366
+ function EmptyState({ icon: Icon, title, subtitle, compact = false, }) {
367
+ return (_jsxs("div", { className: `flex flex-col items-center justify-center gap-1.5 text-center ${compact ? 'px-4 py-6' : 'px-6 py-10'}`, children: [_jsx(Icon, { className: "text-muted-foreground/50 mb-1 h-7 w-7", "aria-hidden": true }), _jsx("p", { className: "text-foreground text-sm font-semibold", children: title }), _jsx("p", { className: "text-muted-foreground max-w-xs text-xs", children: subtitle })] }));
368
+ }
369
+ function DeliveryTile({ icon: Icon, label, value, sub, tone = 'muted', }) {
370
+ const subTone = tone === 'ok'
371
+ ? 'text-emerald-600 dark:text-emerald-400'
372
+ : tone === 'warn'
373
+ ? 'text-amber-600 dark:text-amber-400'
374
+ : tone === 'err'
375
+ ? 'text-red-600 dark:text-red-400'
376
+ : 'text-muted-foreground';
377
+ return (_jsxs("div", { className: "px-4 py-3.5", children: [_jsxs("div", { className: "text-muted-foreground mb-1 flex items-center gap-1.5", children: [_jsx(Icon, { className: "h-3.5 w-3.5", "aria-hidden": true }), _jsx("span", { className: "text-[11px] font-medium", children: label })] }), _jsx("p", { className: "text-foreground text-xl leading-tight font-semibold tracking-tight", children: value }), sub && _jsx("p", { className: `mt-0.5 text-[11px] ${subTone}`, children: sub })] }));
123
378
  }
124
379
  //# sourceMappingURL=Dashboard.js.map