@actuate-media/cms-admin 0.19.0 → 0.21.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 (129) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +13 -9
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/layout/sidebar-posts.test.js +6 -4
  5. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
  6. package/dist/__tests__/lib/seo-service-errors.test.d.ts +2 -0
  7. package/dist/__tests__/lib/seo-service-errors.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/seo-service-errors.test.js +46 -0
  9. package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -0
  10. package/dist/__tests__/views/use-seo-resource.test.d.ts +2 -0
  11. package/dist/__tests__/views/use-seo-resource.test.d.ts.map +1 -0
  12. package/dist/__tests__/views/use-seo-resource.test.js +56 -0
  13. package/dist/__tests__/views/use-seo-resource.test.js.map +1 -0
  14. package/dist/actuate-admin.css +1 -1
  15. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  16. package/dist/components/Breadcrumbs.js +4 -1
  17. package/dist/components/Breadcrumbs.js.map +1 -1
  18. package/dist/components/seo/Drawer.d.ts +11 -0
  19. package/dist/components/seo/Drawer.d.ts.map +1 -0
  20. package/dist/components/seo/Drawer.js +17 -0
  21. package/dist/components/seo/Drawer.js.map +1 -0
  22. package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
  23. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
  24. package/dist/components/seo/RedirectEditorDrawer.js +94 -0
  25. package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
  26. package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
  27. package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
  28. package/dist/components/seo/SeoCopilotDrawer.js +34 -0
  29. package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
  30. package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
  31. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
  32. package/dist/components/seo/SeoEditorDrawer.js +120 -0
  33. package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
  34. package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
  35. package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
  36. package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
  37. package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
  38. package/dist/components/seo/primitives.d.ts +53 -0
  39. package/dist/components/seo/primitives.d.ts.map +1 -0
  40. package/dist/components/seo/primitives.js +101 -0
  41. package/dist/components/seo/primitives.js.map +1 -0
  42. package/dist/layout/Sidebar.d.ts.map +1 -1
  43. package/dist/layout/Sidebar.js +8 -5
  44. package/dist/layout/Sidebar.js.map +1 -1
  45. package/dist/lib/seo-service.d.ts +322 -0
  46. package/dist/lib/seo-service.d.ts.map +1 -0
  47. package/dist/lib/seo-service.js +331 -0
  48. package/dist/lib/seo-service.js.map +1 -0
  49. package/dist/router/index.d.ts.map +1 -1
  50. package/dist/router/index.js +4 -1
  51. package/dist/router/index.js.map +1 -1
  52. package/dist/views/ApiKeys.d.ts +4 -1
  53. package/dist/views/ApiKeys.d.ts.map +1 -1
  54. package/dist/views/ApiKeys.js +5 -3
  55. package/dist/views/ApiKeys.js.map +1 -1
  56. package/dist/views/Dashboard.js +2 -2
  57. package/dist/views/Dashboard.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/NewPostTypeInstructions.js +2 -2
  62. package/dist/views/Posts/post-type-presets.d.ts +5 -0
  63. package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
  64. package/dist/views/Posts/post-type-presets.js +10 -64
  65. package/dist/views/Posts/post-type-presets.js.map +1 -1
  66. package/dist/views/SEO.d.ts.map +1 -1
  67. package/dist/views/SEO.js +89 -132
  68. package/dist/views/SEO.js.map +1 -1
  69. package/dist/views/Settings.d.ts +4 -1
  70. package/dist/views/Settings.d.ts.map +1 -1
  71. package/dist/views/Settings.js +45 -6
  72. package/dist/views/Settings.js.map +1 -1
  73. package/dist/views/Users.d.ts +4 -1
  74. package/dist/views/Users.d.ts.map +1 -1
  75. package/dist/views/Users.js +5 -3
  76. package/dist/views/Users.js.map +1 -1
  77. package/dist/views/seo/AuditTab.d.ts +6 -0
  78. package/dist/views/seo/AuditTab.d.ts.map +1 -0
  79. package/dist/views/seo/AuditTab.js +57 -0
  80. package/dist/views/seo/AuditTab.js.map +1 -0
  81. package/dist/views/seo/ContentTab.d.ts +5 -0
  82. package/dist/views/seo/ContentTab.d.ts.map +1 -0
  83. package/dist/views/seo/ContentTab.js +27 -0
  84. package/dist/views/seo/ContentTab.js.map +1 -0
  85. package/dist/views/seo/OverviewTab.d.ts +8 -0
  86. package/dist/views/seo/OverviewTab.d.ts.map +1 -0
  87. package/dist/views/seo/OverviewTab.js +35 -0
  88. package/dist/views/seo/OverviewTab.js.map +1 -0
  89. package/dist/views/seo/RedirectsTab.d.ts +2 -0
  90. package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
  91. package/dist/views/seo/RedirectsTab.js +130 -0
  92. package/dist/views/seo/RedirectsTab.js.map +1 -0
  93. package/dist/views/seo/TechnicalTab.d.ts +2 -0
  94. package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
  95. package/dist/views/seo/TechnicalTab.js +105 -0
  96. package/dist/views/seo/TechnicalTab.js.map +1 -0
  97. package/dist/views/seo/useSeoResource.d.ts +8 -0
  98. package/dist/views/seo/useSeoResource.d.ts.map +1 -0
  99. package/dist/views/seo/useSeoResource.js +55 -0
  100. package/dist/views/seo/useSeoResource.js.map +1 -0
  101. package/package.json +2 -2
  102. package/src/AdminRoot.tsx +13 -9
  103. package/src/__tests__/layout/sidebar-posts.test.ts +6 -4
  104. package/src/__tests__/lib/seo-service-errors.test.ts +60 -0
  105. package/src/__tests__/views/use-seo-resource.test.tsx +68 -0
  106. package/src/components/Breadcrumbs.tsx +4 -1
  107. package/src/components/seo/Drawer.tsx +65 -0
  108. package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
  109. package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
  110. package/src/components/seo/SeoEditorDrawer.tsx +359 -0
  111. package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
  112. package/src/components/seo/primitives.tsx +240 -0
  113. package/src/layout/Sidebar.tsx +7 -10
  114. package/src/lib/seo-service.ts +660 -0
  115. package/src/router/index.ts +4 -1
  116. package/src/views/ApiKeys.tsx +10 -5
  117. package/src/views/Dashboard.tsx +2 -2
  118. package/src/views/MediaBrowser.tsx +89 -67
  119. package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
  120. package/src/views/Posts/post-type-presets.ts +15 -64
  121. package/src/views/SEO.tsx +186 -863
  122. package/src/views/Settings.tsx +93 -40
  123. package/src/views/Users.tsx +10 -4
  124. package/src/views/seo/AuditTab.tsx +229 -0
  125. package/src/views/seo/ContentTab.tsx +164 -0
  126. package/src/views/seo/OverviewTab.tsx +257 -0
  127. package/src/views/seo/RedirectsTab.tsx +398 -0
  128. package/src/views/seo/TechnicalTab.tsx +315 -0
  129. package/src/views/seo/useSeoResource.ts +66 -0
@@ -0,0 +1,240 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Shared SEO UI primitives — token-only, accessible building blocks used across
5
+ * the five SEO tabs. No raw palette colours: status uses the theme's
6
+ * success / warning / destructive / muted tokens.
7
+ */
8
+ import type { ReactNode } from 'react'
9
+ import { AlertTriangle, Loader2 } from 'lucide-react'
10
+ import type { SeoGrade, SeoSeverity } from '../../lib/seo-service.js'
11
+
12
+ // ─── Grade + severity styling ────────────────────────────────────────
13
+
14
+ export interface GradeMeta {
15
+ label: string
16
+ text: string
17
+ bg: string
18
+ dot: string
19
+ }
20
+
21
+ export function gradeMeta(grade: SeoGrade): GradeMeta {
22
+ switch (grade) {
23
+ case 'good':
24
+ return { label: 'Good', text: 'text-success', bg: 'bg-success/10', dot: 'bg-success' }
25
+ case 'fair':
26
+ return { label: 'Fair', text: 'text-warning', bg: 'bg-warning/10', dot: 'bg-warning' }
27
+ case 'poor':
28
+ return {
29
+ label: 'Poor',
30
+ text: 'text-destructive',
31
+ bg: 'bg-destructive/10',
32
+ dot: 'bg-destructive',
33
+ }
34
+ default:
35
+ return {
36
+ label: 'Critical',
37
+ text: 'text-destructive',
38
+ bg: 'bg-destructive/15',
39
+ dot: 'bg-destructive',
40
+ }
41
+ }
42
+ }
43
+
44
+ /** Grade band from a numeric 0-100 score. */
45
+ export function gradeFromScore(score: number): SeoGrade {
46
+ if (score >= 80) return 'good'
47
+ if (score >= 60) return 'fair'
48
+ if (score >= 1) return 'poor'
49
+ return 'critical'
50
+ }
51
+
52
+ export function severityMeta(severity: SeoSeverity): GradeMeta {
53
+ switch (severity) {
54
+ case 'critical':
55
+ return {
56
+ label: 'Critical',
57
+ text: 'text-destructive',
58
+ bg: 'bg-destructive/10',
59
+ dot: 'bg-destructive',
60
+ }
61
+ case 'warning':
62
+ return { label: 'Warning', text: 'text-warning', bg: 'bg-warning/10', dot: 'bg-warning' }
63
+ default:
64
+ return {
65
+ label: 'Info',
66
+ text: 'text-muted-foreground',
67
+ bg: 'bg-muted',
68
+ dot: 'bg-muted-foreground',
69
+ }
70
+ }
71
+ }
72
+
73
+ // ─── Badges + bars ───────────────────────────────────────────────────
74
+
75
+ export function SeoStatusBadge({
76
+ label,
77
+ tone,
78
+ }: {
79
+ label: string
80
+ tone: 'good' | 'fair' | 'poor' | 'critical' | 'neutral'
81
+ }) {
82
+ const cls =
83
+ tone === 'good'
84
+ ? 'bg-success/10 text-success'
85
+ : tone === 'fair'
86
+ ? 'bg-warning/10 text-warning'
87
+ : tone === 'poor' || tone === 'critical'
88
+ ? 'bg-destructive/10 text-destructive'
89
+ : 'bg-muted text-muted-foreground'
90
+ return (
91
+ <span
92
+ className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${cls}`}
93
+ >
94
+ <span
95
+ className={`h-1.5 w-1.5 rounded-full ${
96
+ tone === 'good'
97
+ ? 'bg-success'
98
+ : tone === 'fair'
99
+ ? 'bg-warning'
100
+ : tone === 'neutral'
101
+ ? 'bg-muted-foreground'
102
+ : 'bg-destructive'
103
+ }`}
104
+ aria-hidden
105
+ />
106
+ {label}
107
+ </span>
108
+ )
109
+ }
110
+
111
+ /** A horizontal score bar (0-100), coloured by grade. */
112
+ export function SeoScoreBar({ score, className = '' }: { score: number; className?: string }) {
113
+ const grade = gradeFromScore(score)
114
+ const fill = grade === 'good' ? 'bg-success' : grade === 'fair' ? 'bg-warning' : 'bg-destructive'
115
+ const clamped = Math.max(0, Math.min(100, Math.round(score)))
116
+ return (
117
+ <div
118
+ className={`bg-muted h-2 w-full overflow-hidden rounded-full ${className}`}
119
+ role="progressbar"
120
+ aria-label="SEO score"
121
+ aria-valuenow={clamped}
122
+ aria-valuemin={0}
123
+ aria-valuemax={100}
124
+ aria-valuetext={`${clamped} out of 100`}
125
+ >
126
+ <div
127
+ className={`h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none ${fill}`}
128
+ style={{ width: `${Math.max(0, Math.min(100, score))}%` }}
129
+ />
130
+ </div>
131
+ )
132
+ }
133
+
134
+ export function ScorePill({ score }: { score: number }) {
135
+ const meta = gradeMeta(gradeFromScore(score))
136
+ return (
137
+ <span
138
+ className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${meta.bg} ${meta.text}`}
139
+ >
140
+ <span className={`h-1.5 w-1.5 rounded-full ${meta.dot}`} aria-hidden />
141
+ {score}
142
+ </span>
143
+ )
144
+ }
145
+
146
+ // ─── Layout cards + states ───────────────────────────────────────────
147
+
148
+ export function SectionCard({
149
+ title,
150
+ description,
151
+ action,
152
+ children,
153
+ className = '',
154
+ }: {
155
+ title?: string
156
+ description?: string
157
+ action?: ReactNode
158
+ children: ReactNode
159
+ className?: string
160
+ }) {
161
+ return (
162
+ <section className={`border-border bg-card rounded-lg border ${className}`}>
163
+ {(title || action) && (
164
+ <header className="border-border flex items-center justify-between gap-3 border-b px-4 py-3">
165
+ <div className="min-w-0">
166
+ {title && <h3 className="text-foreground text-base font-medium">{title}</h3>}
167
+ {description && <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>}
168
+ </div>
169
+ {action}
170
+ </header>
171
+ )}
172
+ <div className="p-4">{children}</div>
173
+ </section>
174
+ )
175
+ }
176
+
177
+ export function SeoLoading({ label = 'Loading…' }: { label?: string }) {
178
+ return (
179
+ <div className="flex h-64 items-center justify-center" role="status" aria-live="polite">
180
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
181
+ <span className="sr-only">{label}</span>
182
+ </div>
183
+ )
184
+ }
185
+
186
+ export function SeoEmptyState({
187
+ icon,
188
+ title,
189
+ description,
190
+ action,
191
+ }: {
192
+ icon?: ReactNode
193
+ title: string
194
+ description?: string
195
+ action?: ReactNode
196
+ }) {
197
+ return (
198
+ <div className="border-border flex flex-col items-center justify-center rounded-lg border border-dashed px-6 py-12 text-center">
199
+ {icon && (
200
+ <div className="text-muted-foreground mb-3" aria-hidden>
201
+ {icon}
202
+ </div>
203
+ )}
204
+ <h3 className="text-foreground text-base font-medium">{title}</h3>
205
+ {description && <p className="text-muted-foreground mt-1 max-w-md text-sm">{description}</p>}
206
+ {action && <div className="mt-4">{action}</div>}
207
+ </div>
208
+ )
209
+ }
210
+
211
+ export function SeoErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) {
212
+ return (
213
+ <div
214
+ className="border-destructive/30 bg-destructive/5 flex items-center gap-3 rounded-lg border p-3"
215
+ role="alert"
216
+ >
217
+ <AlertTriangle className="text-destructive h-5 w-5 shrink-0" aria-hidden />
218
+ <span className="text-destructive flex-1 text-sm">{message}</span>
219
+ {onRetry && (
220
+ <button
221
+ onClick={onRetry}
222
+ className="border-border text-foreground hover:bg-accent rounded-md border px-3 py-1 text-sm transition-colors"
223
+ >
224
+ Retry
225
+ </button>
226
+ )}
227
+ </div>
228
+ )
229
+ }
230
+
231
+ // ─── Button helpers (token-only) ─────────────────────────────────────
232
+
233
+ export const btnPrimary =
234
+ 'inline-flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-base font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60'
235
+
236
+ export const btnSecondary =
237
+ 'inline-flex items-center justify-center gap-2 rounded-md border border-border bg-card px-4 py-2 text-base font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60'
238
+
239
+ export const btnGhostIcon =
240
+ 'inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
@@ -9,7 +9,6 @@ import {
9
9
  Settings,
10
10
  ChevronLeft,
11
11
  ClipboardList,
12
- Users,
13
12
  Search as SearchIcon,
14
13
  Briefcase,
15
14
  FolderOpen,
@@ -22,7 +21,6 @@ import {
22
21
  LayoutGrid,
23
22
  Code2,
24
23
  Library,
25
- KeyRound,
26
24
  ListTree,
27
25
  Menu as MenuIcon,
28
26
  PanelLeftClose,
@@ -156,8 +154,6 @@ const defaultNavItems: NavItem[] = [
156
154
  { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
157
155
  { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
158
156
  { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
159
- { path: '/users', label: 'Users', icon: Users, group: 'Settings' },
160
- { path: '/api-keys', label: 'API Keys', icon: KeyRound, group: 'Settings' },
161
157
  { path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
162
158
  ]
163
159
 
@@ -233,6 +229,9 @@ interface NavRenderContext {
233
229
  }
234
230
 
235
231
  function isPathActive(currentPath: string, itemPath: string): boolean {
232
+ // Ignore query string / hash so state-bearing routes (e.g. `/seo?tab=audit`)
233
+ // still highlight their nav item.
234
+ currentPath = currentPath.split('?')[0]!.split('#')[0]!
236
235
  if (itemPath === '/') return currentPath === '/'
237
236
 
238
237
  // Special case: /posts is a prefix of /posts/types — without this
@@ -457,12 +456,10 @@ export function buildNavItems(config: any): NavItem[] {
457
456
  }
458
457
  for (const bucket of groupBuckets.values()) items.push(...bucket)
459
458
 
460
- // Settings destinations — always last.
461
- items.push(
462
- { path: '/users', label: 'Users', icon: Users, group: 'Settings' },
463
- { path: '/api-keys', label: 'API Keys', icon: KeyRound, group: 'Settings' },
464
- { path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
465
- )
459
+ // Settings destinations — always last. Users + API Keys now live inside
460
+ // the Settings view as tabs (see Settings.tsx), so only Settings is anchored
461
+ // here.
462
+ items.push({ path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' })
466
463
 
467
464
  return items
468
465
  }