@actuate-media/cms-admin 0.49.3 → 0.50.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 (159) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/AdminRoot.js +2 -2
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/components/seo-char-count.test.d.ts +2 -0
  5. package/dist/__tests__/components/seo-char-count.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/seo-char-count.test.js +25 -0
  7. package/dist/__tests__/components/seo-char-count.test.js.map +1 -0
  8. package/dist/__tests__/lib/page-editor-service.test.js +12 -0
  9. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
  10. package/dist/__tests__/lib/post-editor-service.test.js +4 -0
  11. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  12. package/dist/__tests__/views/seo-settings.render.test.js +1 -1
  13. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  14. package/dist/actuate-admin.css +1 -1
  15. package/dist/components/CoauthorResultPopover.js +1 -1
  16. package/dist/components/CoauthorResultPopover.js.map +1 -1
  17. package/dist/components/CommentSidePanel.js +1 -1
  18. package/dist/components/CommentSidePanel.js.map +1 -1
  19. package/dist/components/ContentOverviewChart.js +1 -1
  20. package/dist/components/ContentOverviewChart.js.map +1 -1
  21. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  22. package/dist/components/MediaPickerModal.js +3 -1
  23. package/dist/components/MediaPickerModal.js.map +1 -1
  24. package/dist/components/NotificationBell.js +1 -1
  25. package/dist/components/NotificationBell.js.map +1 -1
  26. package/dist/components/PresenceChips.js +1 -1
  27. package/dist/components/PresenceChips.js.map +1 -1
  28. package/dist/components/SEOConfigPanel.js +2 -2
  29. package/dist/components/SEOConfigPanel.js.map +1 -1
  30. package/dist/components/SEOPanel.d.ts +1 -0
  31. package/dist/components/SEOPanel.d.ts.map +1 -1
  32. package/dist/components/SEOPanel.js +3 -3
  33. package/dist/components/SEOPanel.js.map +1 -1
  34. package/dist/components/SEOPerformance.js +2 -2
  35. package/dist/components/SEOPerformance.js.map +1 -1
  36. package/dist/components/forms/EntriesTable.js +1 -1
  37. package/dist/components/forms/EntriesTable.js.map +1 -1
  38. package/dist/components/seo/SeoCharCount.d.ts +15 -0
  39. package/dist/components/seo/SeoCharCount.d.ts.map +1 -0
  40. package/dist/components/seo/SeoCharCount.js +25 -0
  41. package/dist/components/seo/SeoCharCount.js.map +1 -0
  42. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -1
  43. package/dist/components/seo/SeoEditorDrawer.js +3 -1
  44. package/dist/components/seo/SeoEditorDrawer.js.map +1 -1
  45. package/dist/components/ui/Card.js +1 -1
  46. package/dist/components/ui/Card.js.map +1 -1
  47. package/dist/components/ui/EmptyState.js +1 -1
  48. package/dist/components/ui/EmptyState.js.map +1 -1
  49. package/dist/fields/SlugField.d.ts.map +1 -1
  50. package/dist/fields/SlugField.js +6 -2
  51. package/dist/fields/SlugField.js.map +1 -1
  52. package/dist/layout/Sidebar.js +1 -1
  53. package/dist/layout/Sidebar.js.map +1 -1
  54. package/dist/layout/primitives/PageContainer.js +1 -1
  55. package/dist/layout/primitives/PageContainer.js.map +1 -1
  56. package/dist/lib/page-editor-service.d.ts.map +1 -1
  57. package/dist/lib/page-editor-service.js +6 -0
  58. package/dist/lib/page-editor-service.js.map +1 -1
  59. package/dist/lib/post-editor-service.d.ts.map +1 -1
  60. package/dist/lib/post-editor-service.js +6 -0
  61. package/dist/lib/post-editor-service.js.map +1 -1
  62. package/dist/lib/seo-service.d.ts +3 -1
  63. package/dist/lib/seo-service.d.ts.map +1 -1
  64. package/dist/lib/seo-service.js.map +1 -1
  65. package/dist/views/CollectionList.js +1 -1
  66. package/dist/views/CollectionList.js.map +1 -1
  67. package/dist/views/Dashboard.js +8 -8
  68. package/dist/views/Dashboard.js.map +1 -1
  69. package/dist/views/DocumentEdit.d.ts.map +1 -1
  70. package/dist/views/DocumentEdit.js +2 -1
  71. package/dist/views/DocumentEdit.js.map +1 -1
  72. package/dist/views/ForgotPassword.js +1 -1
  73. package/dist/views/ForgotPassword.js.map +1 -1
  74. package/dist/views/FormEditor.d.ts.map +1 -1
  75. package/dist/views/FormEditor.js +1 -1
  76. package/dist/views/FormEditor.js.map +1 -1
  77. package/dist/views/Login.js +1 -1
  78. package/dist/views/Login.js.map +1 -1
  79. package/dist/views/MediaBrowser.d.ts.map +1 -1
  80. package/dist/views/MediaBrowser.js +3 -3
  81. package/dist/views/MediaBrowser.js.map +1 -1
  82. package/dist/views/PageEditor.js +1 -1
  83. package/dist/views/PageEditor.js.map +1 -1
  84. package/dist/views/Pages/PagesListView.js +3 -3
  85. package/dist/views/Pages/PagesListView.js.map +1 -1
  86. package/dist/views/Pages.js +1 -1
  87. package/dist/views/Pages.js.map +1 -1
  88. package/dist/views/Posts/PostsListView.js +1 -1
  89. package/dist/views/Posts/PostsListView.js.map +1 -1
  90. package/dist/views/Redirects.js +2 -2
  91. package/dist/views/Redirects.js.map +1 -1
  92. package/dist/views/ResetPassword.js +2 -2
  93. package/dist/views/ResetPassword.js.map +1 -1
  94. package/dist/views/ScriptTagEditor.js +1 -1
  95. package/dist/views/ScriptTagEditor.js.map +1 -1
  96. package/dist/views/ScriptTags.js +1 -1
  97. package/dist/views/ScriptTags.js.map +1 -1
  98. package/dist/views/SetupWizard.js +2 -2
  99. package/dist/views/SetupWizard.js.map +1 -1
  100. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
  101. package/dist/views/page-editor/PageSettingsModal.js +12 -9
  102. package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
  103. package/dist/views/page-editor/SectionInspector.js +2 -2
  104. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  105. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  106. package/dist/views/post-editor/PostFieldsModal.js +12 -9
  107. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  108. package/dist/views/post-editor/PostHeader.d.ts.map +1 -1
  109. package/dist/views/post-editor/PostHeader.js +2 -2
  110. package/dist/views/post-editor/PostHeader.js.map +1 -1
  111. package/dist/views/post-editor/PostHeaderPanel.js +1 -1
  112. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
  113. package/package.json +2 -2
  114. package/src/AdminRoot.tsx +2 -2
  115. package/src/__tests__/components/seo-char-count.test.tsx +27 -0
  116. package/src/__tests__/lib/page-editor-service.test.ts +14 -0
  117. package/src/__tests__/lib/post-editor-service.test.ts +9 -0
  118. package/src/__tests__/views/seo-settings.render.test.tsx +1 -1
  119. package/src/components/CoauthorResultPopover.tsx +1 -1
  120. package/src/components/CommentSidePanel.tsx +1 -1
  121. package/src/components/ContentOverviewChart.tsx +1 -1
  122. package/src/components/MediaPickerModal.tsx +5 -1
  123. package/src/components/NotificationBell.tsx +2 -2
  124. package/src/components/PresenceChips.tsx +1 -1
  125. package/src/components/SEOConfigPanel.tsx +3 -3
  126. package/src/components/SEOPanel.tsx +17 -6
  127. package/src/components/SEOPerformance.tsx +5 -5
  128. package/src/components/forms/EntriesTable.tsx +1 -1
  129. package/src/components/seo/SeoCharCount.tsx +44 -0
  130. package/src/components/seo/SeoEditorDrawer.tsx +27 -1
  131. package/src/components/ui/Card.tsx +1 -1
  132. package/src/components/ui/EmptyState.tsx +1 -1
  133. package/src/fields/SlugField.tsx +6 -2
  134. package/src/layout/Sidebar.tsx +1 -1
  135. package/src/layout/primitives/PageContainer.tsx +1 -1
  136. package/src/lib/page-editor-service.ts +6 -0
  137. package/src/lib/post-editor-service.ts +6 -0
  138. package/src/lib/seo-service.ts +9 -1
  139. package/src/views/CollectionList.tsx +1 -1
  140. package/src/views/Dashboard.tsx +12 -12
  141. package/src/views/DocumentEdit.tsx +5 -4
  142. package/src/views/ForgotPassword.tsx +1 -1
  143. package/src/views/FormEditor.tsx +1 -3
  144. package/src/views/Login.tsx +2 -2
  145. package/src/views/MediaBrowser.tsx +7 -9
  146. package/src/views/PageEditor.tsx +2 -2
  147. package/src/views/Pages/PagesListView.tsx +3 -3
  148. package/src/views/Pages.tsx +1 -1
  149. package/src/views/Posts/PostsListView.tsx +1 -1
  150. package/src/views/Redirects.tsx +2 -2
  151. package/src/views/ResetPassword.tsx +2 -2
  152. package/src/views/ScriptTagEditor.tsx +2 -2
  153. package/src/views/ScriptTags.tsx +2 -2
  154. package/src/views/SetupWizard.tsx +2 -2
  155. package/src/views/page-editor/PageSettingsModal.tsx +33 -14
  156. package/src/views/page-editor/SectionInspector.tsx +4 -4
  157. package/src/views/post-editor/PostFieldsModal.tsx +33 -14
  158. package/src/views/post-editor/PostHeader.tsx +2 -4
  159. package/src/views/post-editor/PostHeaderPanel.tsx +2 -2
@@ -1,13 +1,20 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
+ import { normalizeSlug, validateSlug } from '@actuate-media/cms-core/client'
4
5
  import { Modal } from '../../components/ui/Modal.js'
5
6
  import { Button } from '../../components/ui/Button.js'
6
7
  import { MediaPickerModal } from '../../components/MediaPickerModal.js'
7
8
  import { TagInput } from '../../components/TagInput.js'
8
9
  import { RichTextField } from '../../fields/RichTextField.js'
10
+ import { SeoCharCount } from '../../components/seo/SeoCharCount.js'
9
11
  import type { EditorPost } from '../../lib/post-editor-service.js'
10
12
 
13
+ const SEO_TITLE_MIN = 30
14
+ const SEO_TITLE_MAX = 60
15
+ const SEO_DESC_MIN = 120
16
+ const SEO_DESC_MAX = 160
17
+
11
18
  /**
12
19
  * The slice of a collection's field config the modal reads to pick controls:
13
20
  * a `select` category renders a dropdown (structured vocabulary), an `array`
@@ -33,13 +40,7 @@ const LABEL = 'text-foreground mb-1 block text-xs font-medium'
33
40
  const INPUT =
34
41
  'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
35
42
 
36
- function slugify(value: string): string {
37
- return value
38
- .toLowerCase()
39
- .trim()
40
- .replace(/[^a-z0-9]+/g, '-')
41
- .replace(/^-+|-+$/g, '')
42
- }
43
+ const slugify = normalizeSlug
43
44
 
44
45
  /** Edits post-level fields: title, slug, excerpt, featured image, category, tags, body, SEO. */
45
46
  export function PostFieldsModal({
@@ -89,7 +90,10 @@ export function PostFieldsModal({
89
90
  if (!slugTouched) setSlug(slugify(next))
90
91
  }
91
92
 
93
+ const slugError = validateSlug(slug)
94
+
92
95
  function handleSave() {
96
+ if (slugError) return
93
97
  onSave({ title, slug, excerpt, featuredImage, category, tags, body, seoTitle, seoDescription })
94
98
  onClose()
95
99
  }
@@ -104,7 +108,7 @@ export function PostFieldsModal({
104
108
  <Button variant="ghost" onClick={onClose}>
105
109
  Cancel
106
110
  </Button>
107
- <Button variant="primary" onClick={handleSave} disabled={!canEdit}>
111
+ <Button variant="primary" onClick={handleSave} disabled={!canEdit || Boolean(slugError)}>
108
112
  Done
109
113
  </Button>
110
114
  </>
@@ -133,11 +137,18 @@ export function PostFieldsModal({
133
137
  className={INPUT}
134
138
  value={slug}
135
139
  disabled={!canEdit}
140
+ aria-invalid={Boolean(slugError)}
141
+ aria-describedby={slugError ? 'pf-slug-error' : undefined}
136
142
  onChange={(e) => {
137
143
  setSlugTouched(true)
138
144
  setSlug(slugify(e.target.value))
139
145
  }}
140
146
  />
147
+ {slugError && (
148
+ <p id="pf-slug-error" className="text-destructive mt-1 text-xs">
149
+ {slugError}
150
+ </p>
151
+ )}
141
152
  </div>
142
153
  <div>
143
154
  <label className={LABEL} htmlFor="pf-category">
@@ -241,26 +252,34 @@ export function PostFieldsModal({
241
252
  />
242
253
  </div>
243
254
  <div>
244
- <label className={LABEL} htmlFor="pf-seo-title">
245
- SEO title
246
- </label>
255
+ <div className="mb-1 flex items-center justify-between">
256
+ <label className={`${LABEL} mb-0`} htmlFor="pf-seo-title">
257
+ SEO title
258
+ </label>
259
+ <SeoCharCount value={seoTitle} min={SEO_TITLE_MIN} max={SEO_TITLE_MAX} />
260
+ </div>
247
261
  <input
248
262
  id="pf-seo-title"
249
263
  className={INPUT}
250
264
  value={seoTitle}
251
265
  disabled={!canEdit}
266
+ placeholder={title || 'Defaults to the post title'}
252
267
  onChange={(e) => setSeoTitle(e.target.value)}
253
268
  />
254
269
  </div>
255
270
  <div>
256
- <label className={LABEL} htmlFor="pf-seo-desc">
257
- SEO description
258
- </label>
271
+ <div className="mb-1 flex items-center justify-between">
272
+ <label className={`${LABEL} mb-0`} htmlFor="pf-seo-desc">
273
+ SEO description
274
+ </label>
275
+ <SeoCharCount value={seoDescription} min={SEO_DESC_MIN} max={SEO_DESC_MAX} />
276
+ </div>
259
277
  <textarea
260
278
  id="pf-seo-desc"
261
279
  className={`${INPUT} min-h-[60px] resize-y`}
262
280
  value={seoDescription}
263
281
  disabled={!canEdit}
282
+ placeholder={excerpt || 'Summarize the post for search results (120–160 characters)'}
264
283
  onChange={(e) => setSeoDescription(e.target.value)}
265
284
  />
266
285
  </div>
@@ -49,9 +49,7 @@ export function PostHeader({ config, context }: PostHeaderProps) {
49
49
  <div className="absolute inset-0 bg-linear-to-t from-black/70 to-black/10" aria-hidden />
50
50
  <div className="absolute inset-x-0 bottom-0 px-6 pb-8">
51
51
  <div className={`mx-auto flex max-w-3xl flex-col ${align}`}>
52
- <h1 className="text-3xl font-semibold tracking-tight text-white sm:text-4xl">
53
- {title}
54
- </h1>
52
+ <h1 className="text-3xl font-medium tracking-tight text-white sm:text-4xl">{title}</h1>
55
53
  {config.showExcerpt && context.excerpt && (
56
54
  <p className="mt-3 max-w-2xl text-lg text-white/90">{context.excerpt}</p>
57
55
  )}
@@ -67,7 +65,7 @@ export function PostHeader({ config, context }: PostHeaderProps) {
67
65
  <div
68
66
  className={`flex max-w-3xl flex-col ${align} ${config.layout === 'centered' ? 'mx-auto' : ''}`}
69
67
  >
70
- <h1 className="text-3xl font-semibold tracking-tight text-zinc-900 sm:text-4xl">{title}</h1>
68
+ <h1 className="text-3xl font-medium tracking-tight text-zinc-900 sm:text-4xl">{title}</h1>
71
69
  {config.showExcerpt && context.excerpt && (
72
70
  <p className="mt-3 max-w-2xl text-lg text-zinc-600">{context.excerpt}</p>
73
71
  )}
@@ -48,7 +48,7 @@ export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHead
48
48
 
49
49
  <div className="flex-1 space-y-6 overflow-y-auto p-4">
50
50
  <fieldset disabled={!canEdit} className="space-y-3">
51
- <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
51
+ <legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
52
52
  Layout
53
53
  </legend>
54
54
  <div>
@@ -69,7 +69,7 @@ export function PostHeaderPanel({ header, canEdit, onClose, onChange }: PostHead
69
69
  </fieldset>
70
70
 
71
71
  <fieldset disabled={!canEdit} className="space-y-2">
72
- <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
72
+ <legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
73
73
  Show in header
74
74
  </legend>
75
75
  {TOGGLES.map((t) => {