@actuate-media/cms-admin 0.19.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 (94) 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/actuate-admin.css +1 -1
  5. package/dist/components/seo/Drawer.d.ts +11 -0
  6. package/dist/components/seo/Drawer.d.ts.map +1 -0
  7. package/dist/components/seo/Drawer.js +13 -0
  8. package/dist/components/seo/Drawer.js.map +1 -0
  9. package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
  10. package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
  11. package/dist/components/seo/RedirectEditorDrawer.js +94 -0
  12. package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
  13. package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
  14. package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
  15. package/dist/components/seo/SeoCopilotDrawer.js +34 -0
  16. package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
  17. package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
  18. package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
  19. package/dist/components/seo/SeoEditorDrawer.js +111 -0
  20. package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
  21. package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
  22. package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
  23. package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
  24. package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
  25. package/dist/components/seo/primitives.d.ts +53 -0
  26. package/dist/components/seo/primitives.d.ts.map +1 -0
  27. package/dist/components/seo/primitives.js +100 -0
  28. package/dist/components/seo/primitives.js.map +1 -0
  29. package/dist/layout/Sidebar.d.ts.map +1 -1
  30. package/dist/layout/Sidebar.js +3 -0
  31. package/dist/layout/Sidebar.js.map +1 -1
  32. package/dist/lib/seo-service.d.ts +322 -0
  33. package/dist/lib/seo-service.d.ts.map +1 -0
  34. package/dist/lib/seo-service.js +295 -0
  35. package/dist/lib/seo-service.js.map +1 -0
  36. package/dist/router/index.d.ts.map +1 -1
  37. package/dist/router/index.js +4 -1
  38. package/dist/router/index.js.map +1 -1
  39. package/dist/views/MediaBrowser.d.ts.map +1 -1
  40. package/dist/views/MediaBrowser.js +36 -25
  41. package/dist/views/MediaBrowser.js.map +1 -1
  42. package/dist/views/Posts/NewPostTypeInstructions.js +2 -2
  43. package/dist/views/Posts/post-type-presets.d.ts +5 -0
  44. package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
  45. package/dist/views/Posts/post-type-presets.js +10 -64
  46. package/dist/views/Posts/post-type-presets.js.map +1 -1
  47. package/dist/views/SEO.d.ts.map +1 -1
  48. package/dist/views/SEO.js +89 -132
  49. package/dist/views/SEO.js.map +1 -1
  50. package/dist/views/seo/AuditTab.d.ts +6 -0
  51. package/dist/views/seo/AuditTab.d.ts.map +1 -0
  52. package/dist/views/seo/AuditTab.js +57 -0
  53. package/dist/views/seo/AuditTab.js.map +1 -0
  54. package/dist/views/seo/ContentTab.d.ts +5 -0
  55. package/dist/views/seo/ContentTab.d.ts.map +1 -0
  56. package/dist/views/seo/ContentTab.js +27 -0
  57. package/dist/views/seo/ContentTab.js.map +1 -0
  58. package/dist/views/seo/OverviewTab.d.ts +8 -0
  59. package/dist/views/seo/OverviewTab.d.ts.map +1 -0
  60. package/dist/views/seo/OverviewTab.js +35 -0
  61. package/dist/views/seo/OverviewTab.js.map +1 -0
  62. package/dist/views/seo/RedirectsTab.d.ts +2 -0
  63. package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
  64. package/dist/views/seo/RedirectsTab.js +124 -0
  65. package/dist/views/seo/RedirectsTab.js.map +1 -0
  66. package/dist/views/seo/TechnicalTab.d.ts +2 -0
  67. package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
  68. package/dist/views/seo/TechnicalTab.js +102 -0
  69. package/dist/views/seo/TechnicalTab.js.map +1 -0
  70. package/dist/views/seo/useSeoResource.d.ts +8 -0
  71. package/dist/views/seo/useSeoResource.d.ts.map +1 -0
  72. package/dist/views/seo/useSeoResource.js +42 -0
  73. package/dist/views/seo/useSeoResource.js.map +1 -0
  74. package/package.json +2 -2
  75. package/src/AdminRoot.tsx +9 -5
  76. package/src/components/seo/Drawer.tsx +61 -0
  77. package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
  78. package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
  79. package/src/components/seo/SeoEditorDrawer.tsx +347 -0
  80. package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
  81. package/src/components/seo/primitives.tsx +237 -0
  82. package/src/layout/Sidebar.tsx +3 -0
  83. package/src/lib/seo-service.ts +623 -0
  84. package/src/router/index.ts +4 -1
  85. package/src/views/MediaBrowser.tsx +89 -67
  86. package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
  87. package/src/views/Posts/post-type-presets.ts +15 -64
  88. package/src/views/SEO.tsx +186 -863
  89. package/src/views/seo/AuditTab.tsx +229 -0
  90. package/src/views/seo/ContentTab.tsx +164 -0
  91. package/src/views/seo/OverviewTab.tsx +257 -0
  92. package/src/views/seo/RedirectsTab.tsx +392 -0
  93. package/src/views/seo/TechnicalTab.tsx +303 -0
  94. package/src/views/seo/useSeoResource.ts +52 -0
@@ -0,0 +1,237 @@
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
+ return (
116
+ <div
117
+ className={`bg-muted h-2 w-full overflow-hidden rounded-full ${className}`}
118
+ role="progressbar"
119
+ aria-valuenow={score}
120
+ aria-valuemin={0}
121
+ aria-valuemax={100}
122
+ >
123
+ <div
124
+ className={`h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none ${fill}`}
125
+ style={{ width: `${Math.max(0, Math.min(100, score))}%` }}
126
+ />
127
+ </div>
128
+ )
129
+ }
130
+
131
+ export function ScorePill({ score }: { score: number }) {
132
+ const meta = gradeMeta(gradeFromScore(score))
133
+ return (
134
+ <span
135
+ className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${meta.bg} ${meta.text}`}
136
+ >
137
+ <span className={`h-1.5 w-1.5 rounded-full ${meta.dot}`} aria-hidden />
138
+ {score}
139
+ </span>
140
+ )
141
+ }
142
+
143
+ // ─── Layout cards + states ───────────────────────────────────────────
144
+
145
+ export function SectionCard({
146
+ title,
147
+ description,
148
+ action,
149
+ children,
150
+ className = '',
151
+ }: {
152
+ title?: string
153
+ description?: string
154
+ action?: ReactNode
155
+ children: ReactNode
156
+ className?: string
157
+ }) {
158
+ return (
159
+ <section className={`border-border bg-card rounded-lg border ${className}`}>
160
+ {(title || action) && (
161
+ <header className="border-border flex items-center justify-between gap-3 border-b px-4 py-3">
162
+ <div className="min-w-0">
163
+ {title && <h3 className="text-foreground text-base font-medium">{title}</h3>}
164
+ {description && <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>}
165
+ </div>
166
+ {action}
167
+ </header>
168
+ )}
169
+ <div className="p-4">{children}</div>
170
+ </section>
171
+ )
172
+ }
173
+
174
+ export function SeoLoading({ label = 'Loading…' }: { label?: string }) {
175
+ return (
176
+ <div className="flex h-64 items-center justify-center" role="status" aria-live="polite">
177
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
178
+ <span className="sr-only">{label}</span>
179
+ </div>
180
+ )
181
+ }
182
+
183
+ export function SeoEmptyState({
184
+ icon,
185
+ title,
186
+ description,
187
+ action,
188
+ }: {
189
+ icon?: ReactNode
190
+ title: string
191
+ description?: string
192
+ action?: ReactNode
193
+ }) {
194
+ return (
195
+ <div className="border-border flex flex-col items-center justify-center rounded-lg border border-dashed px-6 py-12 text-center">
196
+ {icon && (
197
+ <div className="text-muted-foreground mb-3" aria-hidden>
198
+ {icon}
199
+ </div>
200
+ )}
201
+ <h3 className="text-foreground text-base font-medium">{title}</h3>
202
+ {description && <p className="text-muted-foreground mt-1 max-w-md text-sm">{description}</p>}
203
+ {action && <div className="mt-4">{action}</div>}
204
+ </div>
205
+ )
206
+ }
207
+
208
+ export function SeoErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) {
209
+ return (
210
+ <div
211
+ className="border-destructive/30 bg-destructive/5 flex items-center gap-3 rounded-lg border p-3"
212
+ role="alert"
213
+ >
214
+ <AlertTriangle className="text-destructive h-5 w-5 shrink-0" aria-hidden />
215
+ <span className="text-destructive flex-1 text-sm">{message}</span>
216
+ {onRetry && (
217
+ <button
218
+ onClick={onRetry}
219
+ className="border-border text-foreground hover:bg-accent rounded-md border px-3 py-1 text-sm transition-colors"
220
+ >
221
+ Retry
222
+ </button>
223
+ )}
224
+ </div>
225
+ )
226
+ }
227
+
228
+ // ─── Button helpers (token-only) ─────────────────────────────────────
229
+
230
+ export const btnPrimary =
231
+ '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'
232
+
233
+ export const btnSecondary =
234
+ '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'
235
+
236
+ export const btnGhostIcon =
237
+ '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'
@@ -233,6 +233,9 @@ interface NavRenderContext {
233
233
  }
234
234
 
235
235
  function isPathActive(currentPath: string, itemPath: string): boolean {
236
+ // Ignore query string / hash so state-bearing routes (e.g. `/seo?tab=audit`)
237
+ // still highlight their nav item.
238
+ currentPath = currentPath.split('?')[0]!.split('#')[0]!
236
239
  if (itemPath === '/') return currentPath === '/'
237
240
 
238
241
  // Special case: /posts is a prefix of /posts/types — without this