@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
@@ -584,7 +584,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
584
584
  consistent across navigation. The on-page secondary action row
585
585
  below covers the same creation paths plus authoring shortcuts. */}
586
586
  <div className="flex flex-col gap-1 sm:gap-0.5">
587
- <h1 className="text-foreground text-xl font-semibold tracking-tight sm:text-2xl">
587
+ <h1 className="text-foreground text-xl font-medium tracking-tight sm:text-2xl">
588
588
  {greeting}, {userName} <span aria-hidden>👋</span>
589
589
  </h1>
590
590
  <p className="text-muted-foreground text-sm">
@@ -623,7 +623,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
623
623
  <card.icon className={`h-4 w-4 ${card.iconColor}`} />
624
624
  </div>
625
625
  <div className="flex items-baseline gap-1">
626
- <span className="text-foreground text-2xl leading-none font-semibold tracking-tight">
626
+ <span className="text-foreground text-2xl leading-none font-medium tracking-tight">
627
627
  {card.value}
628
628
  </span>
629
629
  {card.unit && (
@@ -664,7 +664,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
664
664
  <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
665
665
  <header className="border-border flex items-center justify-between border-b px-4 py-3">
666
666
  <div className="min-w-0">
667
- <h2 className="text-foreground text-sm font-semibold">Recent Activity</h2>
667
+ <h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
668
668
  <p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
669
669
  </div>
670
670
  {(stats?.recentDocuments?.length ?? 0) > 8 && (
@@ -693,7 +693,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
693
693
  >
694
694
  <div className="flex min-w-0 items-start gap-3">
695
695
  <div
696
- className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
696
+ className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-medium text-white"
697
697
  style={{ background: it.avatar.color }}
698
698
  aria-hidden
699
699
  >
@@ -701,7 +701,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
701
701
  </div>
702
702
  <div className="min-w-0 flex-1">
703
703
  <p className="text-foreground truncate text-sm leading-snug">
704
- <span className="font-semibold">
704
+ <span className="font-medium">
705
705
  &ldquo;{it.title || 'Untitled'}&rdquo;
706
706
  </span>{' '}
707
707
  <span className="text-muted-foreground">— {it.typeLabel}</span>
@@ -731,7 +731,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
731
731
  <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
732
732
  <header className="border-border flex items-center justify-between border-b px-4 py-3">
733
733
  <div className="min-w-0">
734
- <h2 className="text-foreground text-sm font-semibold">Publishing Queue</h2>
734
+ <h2 className="text-foreground text-sm font-medium">Publishing Queue</h2>
735
735
  <p className="text-muted-foreground mt-0.5 text-xs">
736
736
  {publishQueue.length} item{publishQueue.length === 1 ? '' : 's'} scheduled
737
737
  </p>
@@ -787,7 +787,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
787
787
  <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
788
788
  <header className="border-border flex items-center justify-between border-b px-4 py-3">
789
789
  <div className="min-w-0">
790
- <h2 className="text-foreground text-sm font-semibold">Content Health</h2>
790
+ <h2 className="text-foreground text-sm font-medium">Content Health</h2>
791
791
  <p className="text-muted-foreground mt-0.5 text-xs">SEO &amp; quality issues</p>
792
792
  </div>
793
793
  <button
@@ -799,7 +799,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
799
799
  </header>
800
800
  <div className="px-4 pt-3.5 pb-2.5">
801
801
  <div className="mb-1.5 flex items-baseline justify-between">
802
- <p className="text-foreground text-2xl leading-none font-semibold tracking-tight">
802
+ <p className="text-foreground text-2xl leading-none font-medium tracking-tight">
803
803
  {contentHealth.score > 0 ? contentHealth.score : '—'}
804
804
  {contentHealth.score > 0 && (
805
805
  <span className="text-muted-foreground ml-0.5 text-sm font-normal">/100</span>
@@ -861,7 +861,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
861
861
  {iss.label}
862
862
  </span>
863
863
  <span
864
- className={`shrink-0 text-sm font-semibold ${
864
+ className={`shrink-0 text-sm font-medium ${
865
865
  iss.tone === 'err'
866
866
  ? 'text-red-600 dark:text-red-400'
867
867
  : iss.tone === 'warn'
@@ -892,7 +892,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
892
892
  <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
893
893
  <header className="border-border flex items-center justify-between border-b px-4 py-3">
894
894
  <div className="min-w-0">
895
- <h2 className="text-foreground text-sm font-semibold">Delivery API</h2>
895
+ <h2 className="text-foreground text-sm font-medium">Delivery API</h2>
896
896
  <p className="text-muted-foreground mt-0.5 text-xs">
897
897
  Headless content delivery · last 24 hours
898
898
  </p>
@@ -1030,7 +1030,7 @@ function EmptyState({
1030
1030
  className={`flex flex-col items-center justify-center gap-1.5 text-center ${compact ? 'px-4 py-6' : 'px-6 py-10'}`}
1031
1031
  >
1032
1032
  <Icon className="text-muted-foreground/50 mb-1 h-7 w-7" aria-hidden />
1033
- <p className="text-foreground text-sm font-semibold">{title}</p>
1033
+ <p className="text-foreground text-sm font-medium">{title}</p>
1034
1034
  <p className="text-muted-foreground max-w-xs text-xs">{subtitle}</p>
1035
1035
  </div>
1036
1036
  )
@@ -1065,7 +1065,7 @@ function DeliveryTile({
1065
1065
  <Icon className="h-3.5 w-3.5" aria-hidden />
1066
1066
  <span className="text-[11px] font-medium">{label}</span>
1067
1067
  </div>
1068
- <p className="text-foreground text-xl leading-tight font-semibold tracking-tight">{value}</p>
1068
+ <p className="text-foreground text-xl leading-tight font-medium tracking-tight">{value}</p>
1069
1069
  {sub && (
1070
1070
  <p className={`mt-0.5 inline-flex items-center gap-1 text-[11px] ${subTone}`}>
1071
1071
  {SubIcon && <SubIcon className="h-3 w-3 shrink-0" aria-hidden />}
@@ -114,6 +114,7 @@ export function DocumentEdit({
114
114
  'metaTitle',
115
115
  'metaDescription',
116
116
  'focusKeyphrase',
117
+ 'keyTakeaway',
117
118
  'canonical',
118
119
  'robotsPolicy',
119
120
  'noIndex',
@@ -308,7 +309,7 @@ export function DocumentEdit({
308
309
  <div className="space-y-4">
309
310
  <div className="flex items-center justify-between">
310
311
  <div className="flex items-center gap-3">
311
- <h1 className="text-2xl font-bold">{displayTitle}</h1>
312
+ <h1 className="text-2xl font-medium">{displayTitle}</h1>
312
313
  <span
313
314
  className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${statusColor}`}
314
315
  >
@@ -384,7 +385,7 @@ export function DocumentEdit({
384
385
 
385
386
  <div className="space-y-6">
386
387
  <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
387
- <h3 className="mb-4 font-semibold">Status</h3>
388
+ <h3 className="mb-4 font-medium">Status</h3>
388
389
  <div className="space-y-3">
389
390
  {!isNew && docStatus === 'DRAFT' && (
390
391
  <Button
@@ -436,7 +437,7 @@ export function DocumentEdit({
436
437
 
437
438
  {sidebarFields.length > 0 && (
438
439
  <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
439
- <h3 className="mb-4 font-semibold">Metadata</h3>
440
+ <h3 className="mb-4 font-medium">Metadata</h3>
440
441
  {sidebarFields.map((field: any) => (
441
442
  <div key={field.name} className="mb-4">
442
443
  <FieldRenderer
@@ -460,7 +461,7 @@ export function DocumentEdit({
460
461
 
461
462
  {hasLayout && (
462
463
  <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
463
- <h3 className="mb-1 font-semibold">Layout</h3>
464
+ <h3 className="mb-1 font-medium">Layout</h3>
464
465
  <p className="mb-4 text-xs text-[var(--muted-foreground)]">
465
466
  Assign header/footer variants. Child pages inherit from ancestors.
466
467
  </p>
@@ -48,7 +48,7 @@ export function ForgotPassword({ onNavigate }: ForgotPasswordProps) {
48
48
  <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-xl bg-blue-600">
49
49
  <Shield className="h-7 w-7 text-white" />
50
50
  </div>
51
- <h1 className="text-2xl font-bold text-gray-900">Reset Password</h1>
51
+ <h1 className="text-2xl font-medium text-gray-900">Reset Password</h1>
52
52
  <p className="mt-2 text-gray-600">
53
53
  {sent
54
54
  ? 'Check your inbox for a reset link'
@@ -191,9 +191,7 @@ export function FormEditor({ formId, onNavigate }: FormEditorProps) {
191
191
  <ArrowLeft className="h-5 w-5 text-gray-600" />
192
192
  </button>
193
193
  <div className="flex-1">
194
- <h1 className="text-xl font-semibold text-gray-900">
195
- {isNew ? 'New Form' : 'Edit Form'}
196
- </h1>
194
+ <h1 className="text-xl font-medium text-gray-900">{isNew ? 'New Form' : 'Edit Form'}</h1>
197
195
  </div>
198
196
  <button
199
197
  onClick={handleSave}
@@ -393,7 +393,7 @@ export function Login({
393
393
  ) : (
394
394
  <ActuateBrandLogo className="mx-auto mb-4 h-14 w-auto" />
395
395
  ))}
396
- <h1 className="text-2xl font-bold text-gray-900">{brandName}</h1>
396
+ <h1 className="text-2xl font-medium text-gray-900">{brandName}</h1>
397
397
  <p className="mt-2 text-gray-600">{brandTagline}</p>
398
398
  </div>
399
399
 
@@ -403,7 +403,7 @@ export function Login({
403
403
  className="space-y-5 rounded-xl border border-gray-200 bg-white p-6 shadow-sm"
404
404
  >
405
405
  <div>
406
- <h2 className="text-lg font-semibold text-gray-900">Two-factor authentication</h2>
406
+ <h2 className="text-lg font-medium text-gray-900">Two-factor authentication</h2>
407
407
  <p className="mt-1 text-sm text-gray-600">
408
408
  {useBackupCode
409
409
  ? 'Enter one of your one-time backup codes.'
@@ -411,9 +411,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
411
411
  <FolderInput className="text-muted-foreground h-5 w-5" />
412
412
  </button>
413
413
  <div>
414
- <h1 className="text-foreground mb-1 text-xl font-semibold sm:text-2xl">
415
- Media Library
416
- </h1>
414
+ <h1 className="text-foreground mb-1 text-xl font-medium sm:text-2xl">Media Library</h1>
417
415
  <p className="text-muted-foreground text-sm">{filteredAndSorted.length} files</p>
418
416
  </div>
419
417
  </div>
@@ -745,7 +743,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
745
743
  {panelOpen && activeItem && (
746
744
  <div className="border-border bg-card flex w-80 shrink-0 flex-col overflow-y-auto rounded-lg border lg:w-96">
747
745
  <div className="border-border bg-card sticky top-0 z-10 flex items-center justify-between border-b p-4">
748
- <h3 className="text-foreground truncate text-sm font-semibold">
746
+ <h3 className="text-foreground truncate text-sm font-medium">
749
747
  {activeItem.name}
750
748
  </h3>
751
749
  <button
@@ -773,7 +771,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
773
771
  <div className="flex items-start gap-2">
774
772
  <AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" />
775
773
  <div>
776
- <p className="text-warning mb-1 text-xs font-semibold">
774
+ <p className="text-warning mb-1 text-xs font-medium">
777
775
  {issues.length} issue{issues.length !== 1 ? 's' : ''} found
778
776
  </p>
779
777
  <ul className="space-y-0.5">
@@ -801,7 +799,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
801
799
  )}
802
800
 
803
801
  <div className="border-border space-y-3 border-b p-4">
804
- <h4 className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
802
+ <h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
805
803
  File Information
806
804
  </h4>
807
805
  <div className="grid grid-cols-2 gap-3">
@@ -850,7 +848,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
850
848
  </div>
851
849
 
852
850
  <div className="border-border space-y-4 border-b p-4">
853
- <h4 className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
851
+ <h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
854
852
  SEO & Accessibility
855
853
  </h4>
856
854
 
@@ -917,7 +915,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
917
915
  </div>
918
916
 
919
917
  <div className="border-border border-b p-4">
920
- <h4 className="text-muted-foreground mb-3 text-xs font-semibold tracking-wide uppercase">
918
+ <h4 className="text-muted-foreground mb-3 text-xs font-medium tracking-wide uppercase">
921
919
  Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
922
920
  </h4>
923
921
  {activeItem.usedOn && activeItem.usedOn.length > 0 ? (
@@ -954,7 +952,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
954
952
  </div>
955
953
 
956
954
  <div className="space-y-3 p-4">
957
- <h4 className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
955
+ <h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
958
956
  AI Optimization
959
957
  </h4>
960
958
  <button
@@ -122,7 +122,7 @@ export function PageEditor({ id, onNavigate }: PageEditorProps) {
122
122
  value={title}
123
123
  onChange={(e) => setTitle(e.target.value)}
124
124
  placeholder="Page title"
125
- className="mb-4 w-full border-none px-0 text-2xl font-bold placeholder:text-gray-300 focus:ring-0 focus:outline-none sm:text-3xl"
125
+ className="mb-4 w-full border-none px-0 text-2xl font-medium placeholder:text-gray-300 focus:ring-0 focus:outline-none sm:text-3xl"
126
126
  />
127
127
 
128
128
  <div className="mb-4">
@@ -149,7 +149,7 @@ export function PageEditor({ id, onNavigate }: PageEditorProps) {
149
149
  <div className="w-[30%] overflow-y-auto border-l border-gray-200 bg-gray-50">
150
150
  <div className="space-y-4 p-4">
151
151
  <div className="rounded-lg border border-gray-200 bg-white p-4">
152
- <h3 className="mb-3 text-sm font-semibold text-gray-900">Publish</h3>
152
+ <h3 className="mb-3 text-sm font-medium text-gray-900">Publish</h3>
153
153
  <div className="space-y-3">
154
154
  <div>
155
155
  <label className="mb-1 block text-xs font-medium text-gray-700">Status</label>
@@ -347,7 +347,7 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
347
347
 
348
348
  {/* Tag filter bar */}
349
349
  <div className="flex flex-wrap items-center gap-2">
350
- <span className="text-muted-foreground text-[10px] font-semibold tracking-wider uppercase">
350
+ <span className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
351
351
  Tags
352
352
  </span>
353
353
  {tags.map((t) => (
@@ -637,7 +637,7 @@ function GroupBlock({
637
637
  {showHeader && (
638
638
  <tr className="bg-muted/30 border-border border-b">
639
639
  <td colSpan={7} className="px-3 py-2">
640
- <span className="text-foreground inline-flex items-center gap-2 text-xs font-semibold tracking-wider uppercase">
640
+ <span className="text-foreground inline-flex items-center gap-2 text-xs font-medium tracking-wider uppercase">
641
641
  {color && <span aria-hidden className={`h-2 w-2 rounded-full ${dotFor(color)}`} />}
642
642
  {label}
643
643
  <span className="text-muted-foreground tabular-nums">{count}</span>
@@ -819,7 +819,7 @@ function PageRow({
819
819
  <div className="flex items-center gap-2">
820
820
  <span
821
821
  aria-hidden
822
- className="bg-muted text-muted-foreground inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold"
822
+ className="bg-muted text-muted-foreground inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-medium"
823
823
  >
824
824
  {page.authorInitials}
825
825
  </span>
@@ -279,7 +279,7 @@ export function Pages({ onNavigate }: PagesProps) {
279
279
  <FolderInput className="h-5 w-5 text-gray-600" />
280
280
  </button>
281
281
  <div>
282
- <h1 className="text-xl font-semibold text-gray-900 sm:text-2xl">Pages</h1>
282
+ <h1 className="text-xl font-medium text-gray-900 sm:text-2xl">Pages</h1>
283
283
  <p className="text-sm text-gray-500">
284
284
  {totalCount} page{totalCount !== 1 ? 's' : ''}
285
285
  </p>
@@ -711,7 +711,7 @@ function PostRow({
711
711
  <div className="flex items-center gap-2">
712
712
  <span
713
713
  aria-hidden
714
- className="bg-muted text-muted-foreground inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold"
714
+ className="bg-muted text-muted-foreground inline-flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-medium"
715
715
  >
716
716
  {post.authorInitials}
717
717
  </span>
@@ -143,7 +143,7 @@ export function Redirects({ onNavigate }: RedirectsProps) {
143
143
 
144
144
  <div className="mb-4 flex items-center justify-between">
145
145
  <div>
146
- <h1 className="text-foreground mb-1 text-xl font-semibold sm:text-2xl">Redirects</h1>
146
+ <h1 className="text-foreground mb-1 text-xl font-medium sm:text-2xl">Redirects</h1>
147
147
  <p className="text-muted-foreground text-sm">
148
148
  {filteredAndSorted.length} total redirects
149
149
  </p>
@@ -286,7 +286,7 @@ export function Redirects({ onNavigate }: RedirectsProps) {
286
286
  <Dialog.Portal container={adminPortalContainer()}>
287
287
  <Dialog.Overlay className="fixed inset-0 z-50 bg-black/50" />
288
288
  <Dialog.Content className="bg-card fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg p-6 shadow-lg">
289
- <Dialog.Title className="text-foreground mb-4 text-lg font-semibold">
289
+ <Dialog.Title className="text-foreground mb-4 text-lg font-medium">
290
290
  Add Redirect
291
291
  </Dialog.Title>
292
292
  <form onSubmit={handleSubmit} className="space-y-4">
@@ -25,7 +25,7 @@ export function ResetPassword({ onNavigate, token }: ResetPasswordProps) {
25
25
  <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-xl bg-red-100">
26
26
  <XCircle className="h-7 w-7 text-red-600" />
27
27
  </div>
28
- <h1 className="text-2xl font-bold text-gray-900">Invalid Reset Link</h1>
28
+ <h1 className="text-2xl font-medium text-gray-900">Invalid Reset Link</h1>
29
29
  <p className="mt-2 text-gray-600">
30
30
  This password reset link is invalid or has expired.
31
31
  </p>
@@ -87,7 +87,7 @@ export function ResetPassword({ onNavigate, token }: ResetPasswordProps) {
87
87
  <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-xl bg-blue-600">
88
88
  <Shield className="h-7 w-7 text-white" />
89
89
  </div>
90
- <h1 className="text-2xl font-bold text-gray-900">
90
+ <h1 className="text-2xl font-medium text-gray-900">
91
91
  {success ? 'Password Reset' : 'Choose New Password'}
92
92
  </h1>
93
93
  <p className="mt-2 text-gray-600">
@@ -134,7 +134,7 @@ export function ScriptTagEditor({ tagId, onNavigate }: ScriptTagEditorProps) {
134
134
  <ArrowLeft className="h-4 w-4" />
135
135
  Back to Script Tags
136
136
  </button>
137
- <h1 className="text-foreground text-2xl font-semibold">
137
+ <h1 className="text-foreground text-2xl font-medium">
138
138
  {isNew ? 'New Script Tag' : 'Edit Script Tag'}
139
139
  </h1>
140
140
  </div>
@@ -179,7 +179,7 @@ export function ScriptTagEditor({ tagId, onNavigate }: ScriptTagEditorProps) {
179
179
  </div>
180
180
 
181
181
  <div className="border-border bg-card space-y-4 rounded-lg border p-4">
182
- <h3 className="text-foreground text-sm font-semibold">Placement & Scope</h3>
182
+ <h3 className="text-foreground text-sm font-medium">Placement & Scope</h3>
183
183
 
184
184
  <div>
185
185
  <label className="text-foreground mb-1 block text-sm font-medium">Placement</label>
@@ -95,7 +95,7 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
95
95
 
96
96
  <div className="mb-4 flex items-center justify-between">
97
97
  <div>
98
- <h1 className="text-foreground mb-1 text-2xl font-semibold">Script Tags</h1>
98
+ <h1 className="text-foreground mb-1 text-2xl font-medium">Script Tags</h1>
99
99
  <p className="text-muted-foreground text-sm">
100
100
  Manage tracking codes, analytics, and custom scripts injected into your site
101
101
  </p>
@@ -113,7 +113,7 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
113
113
  {tags.length === 0 && !error ? (
114
114
  <div className="border-border bg-card rounded-lg border p-12 text-center">
115
115
  <Code2 className="text-muted-foreground mx-auto mb-3 h-10 w-10" />
116
- <h3 className="text-foreground text-sm font-semibold">No script tags yet</h3>
116
+ <h3 className="text-foreground text-sm font-medium">No script tags yet</h3>
117
117
  <p className="text-muted-foreground mt-1 text-sm">
118
118
  Add tracking codes like Google Analytics, Tag Manager, or Facebook Pixel.
119
119
  </p>
@@ -74,7 +74,7 @@ export function SetupWizard({ onComplete, siteName = 'Actuate CMS' }: SetupWizar
74
74
  <div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-green-100">
75
75
  <CheckCircle2 className="h-8 w-8 text-green-600" />
76
76
  </div>
77
- <h1 className="mb-2 text-2xl font-bold text-gray-900">Setup Complete</h1>
77
+ <h1 className="mb-2 text-2xl font-medium text-gray-900">Setup Complete</h1>
78
78
  <p className="mb-6 text-gray-600">
79
79
  Your admin account has been created. You can now sign in to manage your content.
80
80
  </p>
@@ -96,7 +96,7 @@ export function SetupWizard({ onComplete, siteName = 'Actuate CMS' }: SetupWizar
96
96
  <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-xl bg-blue-600">
97
97
  <Shield className="h-7 w-7 text-white" />
98
98
  </div>
99
- <h1 className="text-2xl font-bold text-gray-900">Welcome to {siteName}</h1>
99
+ <h1 className="text-2xl font-medium text-gray-900">Welcome to {siteName}</h1>
100
100
  <p className="mt-2 text-gray-600">Create your admin account to get started</p>
101
101
  </div>
102
102
 
@@ -1,10 +1,17 @@
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'
7
+ import { SeoCharCount } from '../../components/seo/SeoCharCount.js'
6
8
  import type { EditorPage } from '../../lib/page-editor-service.js'
7
9
 
10
+ const SEO_TITLE_MIN = 30
11
+ const SEO_TITLE_MAX = 60
12
+ const SEO_DESC_MIN = 120
13
+ const SEO_DESC_MAX = 160
14
+
8
15
  interface PageSettingsModalProps {
9
16
  open: boolean
10
17
  page: EditorPage
@@ -17,13 +24,7 @@ const LABEL = 'text-foreground mb-1 block text-xs font-medium'
17
24
  const INPUT =
18
25
  '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'
19
26
 
20
- function slugify(value: string): string {
21
- return value
22
- .toLowerCase()
23
- .trim()
24
- .replace(/[^a-z0-9]+/g, '-')
25
- .replace(/^-+|-+$/g, '')
26
- }
27
+ const slugify = normalizeSlug
27
28
 
28
29
  /** Edits page-level fields: title, slug, path, and SEO. */
29
30
  export function PageSettingsModal({
@@ -60,7 +61,10 @@ export function PageSettingsModal({
60
61
  }
61
62
  }
62
63
 
64
+ const slugError = validateSlug(slug)
65
+
63
66
  function handleSave() {
67
+ if (slugError) return
64
68
  onSave({ title, slug, path: path || (slug ? `/${slug}` : '/'), seoTitle, seoDescription })
65
69
  onClose()
66
70
  }
@@ -75,7 +79,7 @@ export function PageSettingsModal({
75
79
  <Button variant="ghost" onClick={onClose}>
76
80
  Cancel
77
81
  </Button>
78
- <Button variant="primary" onClick={handleSave} disabled={!canEdit}>
82
+ <Button variant="primary" onClick={handleSave} disabled={!canEdit || Boolean(slugError)}>
79
83
  Done
80
84
  </Button>
81
85
  </>
@@ -104,11 +108,18 @@ export function PageSettingsModal({
104
108
  className={INPUT}
105
109
  value={slug}
106
110
  disabled={!canEdit}
111
+ aria-invalid={Boolean(slugError)}
112
+ aria-describedby={slugError ? 'ps-slug-error' : undefined}
107
113
  onChange={(e) => {
108
114
  setSlugTouched(true)
109
115
  setSlug(slugify(e.target.value))
110
116
  }}
111
117
  />
118
+ {slugError && (
119
+ <p id="ps-slug-error" className="text-destructive mt-1 text-xs">
120
+ {slugError}
121
+ </p>
122
+ )}
112
123
  </div>
113
124
  <div>
114
125
  <label className={LABEL} htmlFor="ps-path">
@@ -125,26 +136,34 @@ export function PageSettingsModal({
125
136
  </div>
126
137
  </div>
127
138
  <div>
128
- <label className={LABEL} htmlFor="ps-seo-title">
129
- SEO title
130
- </label>
139
+ <div className="mb-1 flex items-center justify-between">
140
+ <label className={`${LABEL} mb-0`} htmlFor="ps-seo-title">
141
+ SEO title
142
+ </label>
143
+ <SeoCharCount value={seoTitle} min={SEO_TITLE_MIN} max={SEO_TITLE_MAX} />
144
+ </div>
131
145
  <input
132
146
  id="ps-seo-title"
133
147
  className={INPUT}
134
148
  value={seoTitle}
135
149
  disabled={!canEdit}
150
+ placeholder={title || 'Defaults to the page title'}
136
151
  onChange={(e) => setSeoTitle(e.target.value)}
137
152
  />
138
153
  </div>
139
154
  <div>
140
- <label className={LABEL} htmlFor="ps-seo-desc">
141
- SEO description
142
- </label>
155
+ <div className="mb-1 flex items-center justify-between">
156
+ <label className={`${LABEL} mb-0`} htmlFor="ps-seo-desc">
157
+ SEO description
158
+ </label>
159
+ <SeoCharCount value={seoDescription} min={SEO_DESC_MIN} max={SEO_DESC_MAX} />
160
+ </div>
143
161
  <textarea
144
162
  id="ps-seo-desc"
145
163
  className={`${INPUT} min-h-[64px] resize-y`}
146
164
  value={seoDescription}
147
165
  disabled={!canEdit}
166
+ placeholder="Summarize the page for search results (120–160 characters)"
148
167
  onChange={(e) => setSeoDescription(e.target.value)}
149
168
  />
150
169
  </div>
@@ -139,7 +139,7 @@ export function SectionInspector({
139
139
 
140
140
  {/* Content fields (schema-driven) */}
141
141
  <fieldset disabled={!canEdit} className="space-y-3">
142
- <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
142
+ <legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
143
143
  Content
144
144
  </legend>
145
145
  {def?.fields.map((field) => (
@@ -154,7 +154,7 @@ export function SectionInspector({
154
154
 
155
155
  {/* Section meta + visibility */}
156
156
  <fieldset disabled={!canEdit} className="space-y-3">
157
- <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
157
+ <legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
158
158
  Section settings
159
159
  </legend>
160
160
  <div>
@@ -203,7 +203,7 @@ export function SectionInspector({
203
203
 
204
204
  {/* Appearance */}
205
205
  <fieldset disabled={!canEdit} className="space-y-3">
206
- <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
206
+ <legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
207
207
  Appearance
208
208
  </legend>
209
209
  <div>
@@ -263,7 +263,7 @@ export function SectionInspector({
263
263
 
264
264
  {/* Advanced */}
265
265
  <fieldset disabled={!canEdit} className="space-y-3">
266
- <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
266
+ <legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
267
267
  Advanced
268
268
  </legend>
269
269
  <div>