@actuate-media/cms-admin 0.18.0 → 0.20.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 (167) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +9 -5
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/lib/post-editor-service.test.js +75 -23
  5. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  6. package/dist/__tests__/lib/posts-service.test.js +132 -142
  7. package/dist/__tests__/lib/posts-service.test.js.map +1 -1
  8. package/dist/__tests__/views/posts-list-view.render.test.d.ts +2 -0
  9. package/dist/__tests__/views/posts-list-view.render.test.d.ts.map +1 -0
  10. package/dist/__tests__/views/posts-list-view.render.test.js +73 -0
  11. package/dist/__tests__/views/posts-list-view.render.test.js.map +1 -0
  12. package/dist/actuate-admin.css +1 -1
  13. package/dist/components/seo/Drawer.d.ts +11 -0
  14. package/dist/components/seo/Drawer.d.ts.map +1 -0
  15. package/dist/components/seo/Drawer.js +13 -0
  16. package/dist/components/seo/Drawer.js.map +1 -0
  17. package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
  18. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
  19. package/dist/components/seo/RedirectEditorDrawer.js +94 -0
  20. package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
  21. package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
  22. package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
  23. package/dist/components/seo/SeoCopilotDrawer.js +34 -0
  24. package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
  25. package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
  26. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
  27. package/dist/components/seo/SeoEditorDrawer.js +111 -0
  28. package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
  29. package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
  30. package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
  31. package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
  32. package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
  33. package/dist/components/seo/primitives.d.ts +53 -0
  34. package/dist/components/seo/primitives.d.ts.map +1 -0
  35. package/dist/components/seo/primitives.js +100 -0
  36. package/dist/components/seo/primitives.js.map +1 -0
  37. package/dist/index.d.ts +0 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +0 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/layout/Sidebar.d.ts.map +1 -1
  42. package/dist/layout/Sidebar.js +3 -0
  43. package/dist/layout/Sidebar.js.map +1 -1
  44. package/dist/lib/post-editor-service.d.ts +6 -4
  45. package/dist/lib/post-editor-service.d.ts.map +1 -1
  46. package/dist/lib/post-editor-service.js +32 -37
  47. package/dist/lib/post-editor-service.js.map +1 -1
  48. package/dist/lib/posts-service.d.ts.map +1 -1
  49. package/dist/lib/posts-service.js +91 -160
  50. package/dist/lib/posts-service.js.map +1 -1
  51. package/dist/lib/seo-service.d.ts +322 -0
  52. package/dist/lib/seo-service.d.ts.map +1 -0
  53. package/dist/lib/seo-service.js +295 -0
  54. package/dist/lib/seo-service.js.map +1 -0
  55. package/dist/router/index.d.ts.map +1 -1
  56. package/dist/router/index.js +4 -1
  57. package/dist/router/index.js.map +1 -1
  58. package/dist/views/MediaBrowser.d.ts.map +1 -1
  59. package/dist/views/MediaBrowser.js +36 -25
  60. package/dist/views/MediaBrowser.js.map +1 -1
  61. package/dist/views/Posts/NewPostPicker.d.ts +5 -4
  62. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -1
  63. package/dist/views/Posts/NewPostPicker.js +5 -4
  64. package/dist/views/Posts/NewPostPicker.js.map +1 -1
  65. package/dist/views/Posts/NewPostTypeInstructions.js +2 -2
  66. package/dist/views/Posts/PostTypesView.d.ts +5 -4
  67. package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
  68. package/dist/views/Posts/PostTypesView.js.map +1 -1
  69. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  70. package/dist/views/Posts/PostsListView.js +8 -0
  71. package/dist/views/Posts/PostsListView.js.map +1 -1
  72. package/dist/views/Posts/post-type-presets.d.ts +5 -0
  73. package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
  74. package/dist/views/Posts/post-type-presets.js +10 -64
  75. package/dist/views/Posts/post-type-presets.js.map +1 -1
  76. package/dist/views/SEO.d.ts.map +1 -1
  77. package/dist/views/SEO.js +89 -132
  78. package/dist/views/SEO.js.map +1 -1
  79. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +6 -2
  80. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -1
  81. package/dist/views/page-editor/sections/ArticleBodySection.js +8 -8
  82. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -1
  83. package/dist/views/post-editor/PostEditorCanvas.js +7 -1
  84. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  85. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  86. package/dist/views/post-editor/PostFieldsModal.js +10 -4
  87. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  88. package/dist/views/post-editor/PostHeader.d.ts.map +1 -1
  89. package/dist/views/post-editor/PostHeader.js +4 -3
  90. package/dist/views/post-editor/PostHeader.js.map +1 -1
  91. package/dist/views/post-editor/PostPreview.d.ts.map +1 -1
  92. package/dist/views/post-editor/PostPreview.js +11 -1
  93. package/dist/views/post-editor/PostPreview.js.map +1 -1
  94. package/dist/views/post-editor/PostSectionEditor.d.ts +22 -2
  95. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  96. package/dist/views/post-editor/PostSectionEditor.js +4 -1
  97. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  98. package/dist/views/seo/AuditTab.d.ts +6 -0
  99. package/dist/views/seo/AuditTab.d.ts.map +1 -0
  100. package/dist/views/seo/AuditTab.js +57 -0
  101. package/dist/views/seo/AuditTab.js.map +1 -0
  102. package/dist/views/seo/ContentTab.d.ts +5 -0
  103. package/dist/views/seo/ContentTab.d.ts.map +1 -0
  104. package/dist/views/seo/ContentTab.js +27 -0
  105. package/dist/views/seo/ContentTab.js.map +1 -0
  106. package/dist/views/seo/OverviewTab.d.ts +8 -0
  107. package/dist/views/seo/OverviewTab.d.ts.map +1 -0
  108. package/dist/views/seo/OverviewTab.js +35 -0
  109. package/dist/views/seo/OverviewTab.js.map +1 -0
  110. package/dist/views/seo/RedirectsTab.d.ts +2 -0
  111. package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
  112. package/dist/views/seo/RedirectsTab.js +124 -0
  113. package/dist/views/seo/RedirectsTab.js.map +1 -0
  114. package/dist/views/seo/TechnicalTab.d.ts +2 -0
  115. package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
  116. package/dist/views/seo/TechnicalTab.js +102 -0
  117. package/dist/views/seo/TechnicalTab.js.map +1 -0
  118. package/dist/views/seo/useSeoResource.d.ts +8 -0
  119. package/dist/views/seo/useSeoResource.d.ts.map +1 -0
  120. package/dist/views/seo/useSeoResource.js +42 -0
  121. package/dist/views/seo/useSeoResource.js.map +1 -0
  122. package/package.json +2 -2
  123. package/src/AdminRoot.tsx +9 -5
  124. package/src/__tests__/lib/post-editor-service.test.ts +78 -23
  125. package/src/__tests__/lib/posts-service.test.ts +134 -163
  126. package/src/__tests__/views/posts-list-view.render.test.tsx +84 -0
  127. package/src/components/seo/Drawer.tsx +61 -0
  128. package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
  129. package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
  130. package/src/components/seo/SeoEditorDrawer.tsx +347 -0
  131. package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
  132. package/src/components/seo/primitives.tsx +237 -0
  133. package/src/index.ts +0 -2
  134. package/src/layout/Sidebar.tsx +3 -0
  135. package/src/lib/post-editor-service.ts +41 -40
  136. package/src/lib/posts-service.ts +109 -180
  137. package/src/lib/seo-service.ts +623 -0
  138. package/src/router/index.ts +4 -1
  139. package/src/views/MediaBrowser.tsx +89 -67
  140. package/src/views/Posts/NewPostPicker.tsx +5 -4
  141. package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
  142. package/src/views/Posts/PostTypesView.tsx +5 -4
  143. package/src/views/Posts/PostsListView.tsx +9 -0
  144. package/src/views/Posts/post-type-presets.ts +15 -64
  145. package/src/views/SEO.tsx +186 -863
  146. package/src/views/page-editor/sections/ArticleBodySection.tsx +12 -16
  147. package/src/views/post-editor/PostEditorCanvas.tsx +7 -1
  148. package/src/views/post-editor/PostFieldsModal.tsx +36 -21
  149. package/src/views/post-editor/PostHeader.tsx +4 -4
  150. package/src/views/post-editor/PostPreview.tsx +12 -1
  151. package/src/views/post-editor/PostSectionEditor.tsx +22 -5
  152. package/src/views/seo/AuditTab.tsx +229 -0
  153. package/src/views/seo/ContentTab.tsx +164 -0
  154. package/src/views/seo/OverviewTab.tsx +257 -0
  155. package/src/views/seo/RedirectsTab.tsx +392 -0
  156. package/src/views/seo/TechnicalTab.tsx +303 -0
  157. package/src/views/seo/useSeoResource.ts +52 -0
  158. package/dist/views/PostEditor.d.ts +0 -6
  159. package/dist/views/PostEditor.d.ts.map +0 -1
  160. package/dist/views/PostEditor.js +0 -79
  161. package/dist/views/PostEditor.js.map +0 -1
  162. package/dist/views/Posts.d.ts +0 -5
  163. package/dist/views/Posts.d.ts.map +0 -1
  164. package/dist/views/Posts.js +0 -73
  165. package/dist/views/Posts.js.map +0 -1
  166. package/src/views/PostEditor.tsx +0 -257
  167. package/src/views/Posts.tsx +0 -375
@@ -30,7 +30,6 @@ import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
30
30
  import { useApiData } from '../lib/useApiData.js'
31
31
  import { cmsApi } from '../lib/api.js'
32
32
  import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
33
- import { FocalPointPicker } from '../components/FocalPointPicker.js'
34
33
 
35
34
  interface MediaItem {
36
35
  id: number | string
@@ -120,8 +119,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
120
119
  const [editAlt, setEditAlt] = useState('')
121
120
  const [editTitle, setEditTitle] = useState('')
122
121
  const [editFilename, setEditFilename] = useState('')
123
- const [focalX, setFocalX] = useState(0.5)
124
- const [focalY, setFocalY] = useState(0.5)
125
122
  const [saving, setSaving] = useState(false)
126
123
  const [aiGenerating, setAiGenerating] = useState<string | null>(null)
127
124
  const [uploading, setUploading] = useState(false)
@@ -157,8 +154,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
157
154
  setEditAlt(item.altTag ?? '')
158
155
  setEditTitle(item.title ?? '')
159
156
  setEditFilename(item.name)
160
- setFocalX((item as any).focalPointX ?? 0.5)
161
- setFocalY((item as any).focalPointY ?? 0.5)
162
157
  }
163
158
 
164
159
  const closeDetail = () => {
@@ -187,8 +182,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
187
182
  alt: editAlt,
188
183
  title: editTitle,
189
184
  filename: editFilename,
190
- focalX,
191
- focalY,
192
185
  }),
193
186
  })
194
187
  setSaving(false)
@@ -244,18 +237,38 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
244
237
  return
245
238
  }
246
239
 
247
- await new Promise((r) => setTimeout(r, 1500))
240
+ if (!activeItem) {
241
+ setAiGenerating(null)
242
+ return
243
+ }
244
+
245
+ const res = await cmsApi<{ alt?: string; title?: string; alternatives?: string[] }>(
246
+ '/ai/media-metadata',
247
+ { method: 'POST', body: JSON.stringify({ mediaId: activeItem.id }) },
248
+ )
249
+
250
+ if (res.error) {
251
+ // AI plugin/provider not configured (e.g. local dev): fall back to a
252
+ // basic filename-derived suggestion so the action still helps.
253
+ const base = (activeItem.name ?? '')
254
+ .replace(/\.\w+$/, '')
255
+ .replace(/[-_]/g, ' ')
256
+ .trim()
257
+ if (field === 'alt') {
258
+ setEditAlt(base ? `Image of ${base}` : '')
259
+ } else {
260
+ setEditTitle(base.replace(/\b\w/g, (c) => c.toUpperCase()))
261
+ }
262
+ toast.info('AI unavailable — generated a basic suggestion from the filename')
263
+ setAiGenerating(null)
264
+ return
265
+ }
266
+
248
267
  if (field === 'alt') {
249
- const generated = `A ${(activeItem?.format ?? 'media').toLowerCase()} image showing ${(activeItem?.name ?? 'uploaded').replace(/[-_]/g, ' ').replace(/\.\w+$/, '')} content`
250
- setEditAlt(generated)
268
+ setEditAlt(res.data?.alt ?? '')
251
269
  toast.success('Alt tag generated by AI')
252
- } else if (field === 'title') {
253
- const generated =
254
- activeItem?.name
255
- .replace(/[-_]/g, ' ')
256
- .replace(/\.\w+$/, '')
257
- .replace(/\b\w/g, (c) => c.toUpperCase()) ?? ''
258
- setEditTitle(generated)
270
+ } else {
271
+ setEditTitle(res.data?.title ?? '')
259
272
  toast.success('Title generated by AI')
260
273
  }
261
274
  setAiGenerating(null)
@@ -533,7 +546,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
533
546
  >
534
547
  {viewMode === 'grid' ? (
535
548
  <div
536
- className={`grid h-full gap-2 overflow-y-auto p-2 sm:gap-3 sm:p-3 ${
549
+ className={`grid h-full auto-rows-min content-start gap-3 overflow-y-auto p-2 sm:gap-4 sm:p-3 ${
537
550
  panelOpen
538
551
  ? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
539
552
  : 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
@@ -542,10 +555,17 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
542
555
  {filteredAndSorted.map((item) => {
543
556
  const isActive = activeItem?.id === item.id
544
557
  const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
558
+ const typeLabel = (
559
+ item.name.includes('.')
560
+ ? item.name.split('.').pop()!
561
+ : item.format || item.type || ''
562
+ )
563
+ .replace(/^.*\//, '')
564
+ .toUpperCase()
545
565
  return (
546
566
  <div
547
567
  key={item.id}
548
- className={`group relative aspect-square cursor-pointer overflow-hidden rounded-lg border-2 transition-all ${
568
+ className={`group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border-2 transition-all ${
549
569
  isActive
550
570
  ? 'border-blue-500 ring-2 ring-blue-200'
551
571
  : selectedMedia.includes(item.id)
@@ -556,45 +576,61 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
556
576
  draggable
557
577
  onDragStart={(e) => handleDragStart(e, item.id)}
558
578
  >
559
- <div className="flex h-full w-full items-center justify-center bg-gray-100">
560
- <MediaPreview item={item} />
561
- </div>
562
- <div className="absolute inset-0 bg-linear-to-t from-black/60 to-transparent opacity-0 transition-opacity group-hover:opacity-100">
563
- <div className="absolute right-0 bottom-0 left-0 p-2">
564
- <p className="truncate text-xs font-medium text-white">{item.name}</p>
565
- <p className="text-xs text-white/80">{item.size}</p>
579
+ <div className="relative aspect-square w-full overflow-hidden">
580
+ <div className="flex h-full w-full items-center justify-center bg-gray-100">
581
+ <MediaPreview item={item} />
566
582
  </div>
567
- </div>
568
- {hasIssues && (
583
+ {hasIssues && (
584
+ <div
585
+ className="absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500"
586
+ title="Needs attention"
587
+ >
588
+ <AlertTriangle className="h-3 w-3 text-white" />
589
+ </div>
590
+ )}
569
591
  <div
570
- className="absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500"
571
- title="Needs attention"
592
+ className="absolute top-1.5 right-1.5 opacity-0 transition-opacity group-hover:opacity-100"
593
+ onClick={(e) => handleCheckbox(e, item.id)}
572
594
  >
573
- <AlertTriangle className="h-3 w-3 text-white" />
595
+ <div
596
+ className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
597
+ selectedMedia.includes(item.id)
598
+ ? 'border-blue-600 bg-blue-600'
599
+ : 'border-gray-400 bg-white/80'
600
+ }`}
601
+ >
602
+ {selectedMedia.includes(item.id) && (
603
+ <svg
604
+ className="h-3 w-3 text-white"
605
+ fill="none"
606
+ viewBox="0 0 12 12"
607
+ >
608
+ <path
609
+ d="M10 3L4.5 8.5L2 6"
610
+ stroke="currentColor"
611
+ strokeWidth="2"
612
+ strokeLinecap="round"
613
+ strokeLinejoin="round"
614
+ />
615
+ </svg>
616
+ )}
617
+ </div>
574
618
  </div>
575
- )}
576
- <div
577
- className="absolute top-1.5 right-1.5 opacity-0 transition-opacity group-hover:opacity-100"
578
- onClick={(e) => handleCheckbox(e, item.id)}
579
- >
580
- <div
581
- className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
582
- selectedMedia.includes(item.id)
583
- ? 'border-blue-600 bg-blue-600'
584
- : 'border-gray-400 bg-white/80'
619
+ </div>
620
+ <div className="flex items-center gap-2 border-t border-gray-200 bg-white px-2 py-1.5">
621
+ <span
622
+ className={`mt-0.5 h-2 w-2 shrink-0 self-start rounded-full ${
623
+ hasIssues ? 'bg-yellow-500' : 'bg-green-500'
585
624
  }`}
586
- >
587
- {selectedMedia.includes(item.id) && (
588
- <svg className="h-3 w-3 text-white" fill="none" viewBox="0 0 12 12">
589
- <path
590
- d="M10 3L4.5 8.5L2 6"
591
- stroke="currentColor"
592
- strokeWidth="2"
593
- strokeLinecap="round"
594
- strokeLinejoin="round"
595
- />
596
- </svg>
597
- )}
625
+ title={hasIssues ? 'Needs attention' : 'SEO complete'}
626
+ />
627
+ <div className="min-w-0 flex-1">
628
+ <p className="truncate text-xs font-medium text-gray-900">
629
+ {item.name}
630
+ </p>
631
+ <p className="truncate text-xs text-gray-500">
632
+ {[typeLabel, item.size].filter(Boolean).join(' · ')}
633
+ </p>
598
634
  </div>
599
635
  </div>
600
636
  </div>
@@ -872,20 +908,6 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
872
908
  </div>
873
909
  </div>
874
910
 
875
- {isImageMedia(activeItem) && activeItem.url && (
876
- <div className="border-b border-gray-200 p-4">
877
- <FocalPointPicker
878
- imageUrl={activeItem.url}
879
- focalX={focalX}
880
- focalY={focalY}
881
- onChange={(x, y) => {
882
- setFocalX(x)
883
- setFocalY(y)
884
- }}
885
- />
886
- </div>
887
- )}
888
-
889
911
  <div className="border-b border-gray-200 p-4">
890
912
  <h4 className="mb-3 text-xs font-semibold tracking-wide text-gray-500 uppercase">
891
913
  Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
@@ -9,10 +9,11 @@ import { resolvePostTypeIcon } from '../../components/posts/icons.js'
9
9
  /**
10
10
  * Type-picker landing page for "+ New Post".
11
11
  *
12
- * When zero or one post type is configured we'd ordinarily auto-route
13
- * to the per-collection editor, but the spec calls out always showing
14
- * a chooser when "+ New Post" is clicked. We keep that behavior so
15
- * the experience is consistent regardless of how many types exist.
12
+ * Behavior by number of configured post types:
13
+ * - exactly one → skip the picker and forward straight to that type's
14
+ * new-post editor (a one-card chooser adds nothing but a flash)
15
+ * - two or more → show the chooser cards
16
+ * - zero → show a "no post types configured" setup hint
16
17
  *
17
18
  * Picking a card navigates to the section-based Post editor's new-post
18
19
  * route (`/posts/<slug>/new`), which seeds the post from that type's
@@ -11,8 +11,8 @@ import { POST_TYPE_PRESETS, type PresetColor, type QuickPreset } from './post-ty
11
11
  * /posts/types/new — Quick Post Type gallery + snippet generator.
12
12
  *
13
13
  * Two ways to start:
14
- * 1. Pick a Quick Post Type preset (Case Studies, News, Portfolio,
15
- * Events, Team) — pre-fills the form and tailored extra fields.
14
+ * 1. Pick a Quick Post Type preset (Blog) pre-fills the form and
15
+ * tailored extra fields.
16
16
  * 2. Fill the form from scratch.
17
17
  *
18
18
  * Either way the page emits a paste-ready `actuate.config.ts` snippet.
@@ -14,10 +14,11 @@ import { resolvePostTypeIcon } from '../../components/posts/icons.js'
14
14
  * - title + description
15
15
  * - footer row: post count · field count · URL base
16
16
  *
17
- * Cards open the per-collection settings (`/collections/:slug/settings`
18
- * in the existing admin) when clicked. The dashed "+ New Post Type"
19
- * card routes to /posts/types/new where users get instructions for
20
- * adding a new collection to the config file.
17
+ * Clicking a card opens that type's settings (`/posts/types/:slug/edit`);
18
+ * each card also has an "Edit Template" action (`/posts/types/:slug/template`)
19
+ * for the type's default layout. The dashed "+ New Post Type" card routes to
20
+ * `/posts/types/new`, where users get instructions for adding a new collection
21
+ * to the config file.
21
22
  *
22
23
  * Why no in-app create/edit flow yet:
23
24
  * Collections are file-defined (actuate.config.ts). Writing to that
@@ -118,6 +118,15 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
118
118
  setSelected(new Set())
119
119
  }, [activeType, debouncedSearch, status, author])
120
120
 
121
+ // Selection is scoped to the rows visible on the current page: bulk
122
+ // actions only ever touch `selectedPosts` (the current page's selected
123
+ // rows), so clear the selection whenever the page changes. Without this
124
+ // the bulk bar count keeps counting rows from earlier pages while the
125
+ // publish/delete handlers silently operate on nothing.
126
+ useEffect(() => {
127
+ setSelected(new Set())
128
+ }, [page])
129
+
121
130
  const total = data?.total ?? 0
122
131
  const totalAll = useMemo(
123
132
  () => (data?.types ?? []).reduce((sum, t) => sum + t.postCount, 0),
@@ -10,6 +10,11 @@ import type { PostTypeIconName } from '../../components/posts/icons.js'
10
10
  * Presets are NOT a runtime data model — they only seed the snippet.
11
11
  * Collections stay code-defined; this just saves the developer from
12
12
  * hand-authoring common field sets.
13
+ *
14
+ * Only the canonical **Blog** post type ships as a built-in preset. Other
15
+ * content types (case studies, portfolio, team, …) are available as opt-in
16
+ * collection factories in `@actuate-media/cms-core` (`collections/presets.ts`)
17
+ * rather than shipped here as defaults.
13
18
  */
14
19
 
15
20
  export type PresetColor = 'purple' | 'green' | 'orange' | 'blue' | 'red' | 'pink'
@@ -34,72 +39,18 @@ export interface QuickPreset {
34
39
 
35
40
  export const POST_TYPE_PRESETS: QuickPreset[] = [
36
41
  {
37
- key: 'case-studies',
38
- label: 'Case Studies',
39
- singular: 'Case Study',
40
- plural: 'Case Studies',
41
- slug: 'case-studies',
42
- description: 'Client success stories and detailed project case studies.',
43
- color: 'green',
44
- icon: 'briefcase',
45
- extraFields: [
46
- "client: { type: 'text', label: 'Client Name' },",
47
- "industry: { type: 'text', label: 'Industry' },",
48
- "results: { type: 'richText', label: 'Results & Outcomes' },",
49
- ],
50
- },
51
- {
52
- key: 'news',
53
- label: 'News',
54
- singular: 'News Item',
55
- plural: 'News',
56
- slug: 'news',
57
- description: 'Company announcements, press releases, and industry news.',
58
- color: 'orange',
59
- icon: 'newspaper',
60
- extraFields: ["source: { type: 'text', label: 'Source' },"],
61
- },
62
- {
63
- key: 'portfolio',
64
- label: 'Portfolio',
65
- singular: 'Portfolio Item',
66
- plural: 'Portfolio',
67
- slug: 'portfolio',
68
- description: 'Showcase projects, work samples, and creative pieces.',
69
- color: 'blue',
70
- icon: 'image',
71
- extraFields: [
72
- "projectUrl: { type: 'url', label: 'Project URL' },",
73
- "gallery: { type: 'media', label: 'Gallery' },",
74
- ],
75
- },
76
- {
77
- key: 'events',
78
- label: 'Events',
79
- singular: 'Event',
80
- plural: 'Events',
81
- slug: 'events',
82
- description: 'Webinars, conferences, and scheduled happenings.',
83
- color: 'pink',
84
- icon: 'calendar',
85
- extraFields: [
86
- "startDate: { type: 'date', label: 'Start Date', includeTime: true },",
87
- "endDate: { type: 'date', label: 'End Date', includeTime: true },",
88
- "location: { type: 'text', label: 'Location' },",
89
- ],
90
- },
91
- {
92
- key: 'team',
93
- label: 'Team Members',
94
- singular: 'Team Member',
95
- plural: 'Team',
96
- slug: 'team',
97
- description: 'Staff bios, headshots, and role information.',
42
+ key: 'blog',
43
+ label: 'Blog',
44
+ singular: 'Post',
45
+ plural: 'Posts',
46
+ slug: 'blog',
47
+ description: 'Long-form articles and blog posts with author, category, and tags.',
98
48
  color: 'purple',
99
- icon: 'users',
49
+ icon: 'newspaper',
100
50
  extraFields: [
101
- "role: { type: 'text', label: 'Role' },",
102
- "headshot: { type: 'media', label: 'Headshot' },",
51
+ "author: { type: 'text', label: 'Author' },",
52
+ "category: { type: 'text', label: 'Category' },",
53
+ "tags: { type: 'array', label: 'Tags', fields: { value: { type: 'text', label: 'Tag' } } },",
103
54
  ],
104
55
  },
105
56
  ]