@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.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +9 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/seo/Drawer.d.ts +11 -0
- package/dist/components/seo/Drawer.d.ts.map +1 -0
- package/dist/components/seo/Drawer.js +13 -0
- package/dist/components/seo/Drawer.js.map +1 -0
- package/dist/components/seo/RedirectEditorDrawer.d.ts +9 -0
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -0
- package/dist/components/seo/RedirectEditorDrawer.js +94 -0
- package/dist/components/seo/RedirectEditorDrawer.js.map +1 -0
- package/dist/components/seo/SeoCopilotDrawer.d.ts +10 -0
- package/dist/components/seo/SeoCopilotDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoCopilotDrawer.js +34 -0
- package/dist/components/seo/SeoCopilotDrawer.js.map +1 -0
- package/dist/components/seo/SeoEditorDrawer.d.ts +8 -0
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorDrawer.js +111 -0
- package/dist/components/seo/SeoEditorDrawer.js.map +1 -0
- package/dist/components/seo/SeoIssueFixDrawer.d.ts +8 -0
- package/dist/components/seo/SeoIssueFixDrawer.d.ts.map +1 -0
- package/dist/components/seo/SeoIssueFixDrawer.js +111 -0
- package/dist/components/seo/SeoIssueFixDrawer.js.map +1 -0
- package/dist/components/seo/primitives.d.ts +53 -0
- package/dist/components/seo/primitives.d.ts.map +1 -0
- package/dist/components/seo/primitives.js +100 -0
- package/dist/components/seo/primitives.js.map +1 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +3 -0
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/seo-service.d.ts +322 -0
- package/dist/lib/seo-service.d.ts.map +1 -0
- package/dist/lib/seo-service.js +295 -0
- package/dist/lib/seo-service.js.map +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +4 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +36 -25
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js +2 -2
- package/dist/views/Posts/post-type-presets.d.ts +5 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -1
- package/dist/views/Posts/post-type-presets.js +10 -64
- package/dist/views/Posts/post-type-presets.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +89 -132
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/seo/AuditTab.d.ts +6 -0
- package/dist/views/seo/AuditTab.d.ts.map +1 -0
- package/dist/views/seo/AuditTab.js +57 -0
- package/dist/views/seo/AuditTab.js.map +1 -0
- package/dist/views/seo/ContentTab.d.ts +5 -0
- package/dist/views/seo/ContentTab.d.ts.map +1 -0
- package/dist/views/seo/ContentTab.js +27 -0
- package/dist/views/seo/ContentTab.js.map +1 -0
- package/dist/views/seo/OverviewTab.d.ts +8 -0
- package/dist/views/seo/OverviewTab.d.ts.map +1 -0
- package/dist/views/seo/OverviewTab.js +35 -0
- package/dist/views/seo/OverviewTab.js.map +1 -0
- package/dist/views/seo/RedirectsTab.d.ts +2 -0
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -0
- package/dist/views/seo/RedirectsTab.js +124 -0
- package/dist/views/seo/RedirectsTab.js.map +1 -0
- package/dist/views/seo/TechnicalTab.d.ts +2 -0
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -0
- package/dist/views/seo/TechnicalTab.js +102 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -0
- package/dist/views/seo/useSeoResource.d.ts +8 -0
- package/dist/views/seo/useSeoResource.d.ts.map +1 -0
- package/dist/views/seo/useSeoResource.js +42 -0
- package/dist/views/seo/useSeoResource.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +9 -5
- package/src/components/seo/Drawer.tsx +61 -0
- package/src/components/seo/RedirectEditorDrawer.tsx +235 -0
- package/src/components/seo/SeoCopilotDrawer.tsx +76 -0
- package/src/components/seo/SeoEditorDrawer.tsx +347 -0
- package/src/components/seo/SeoIssueFixDrawer.tsx +244 -0
- package/src/components/seo/primitives.tsx +237 -0
- package/src/layout/Sidebar.tsx +3 -0
- package/src/lib/seo-service.ts +623 -0
- package/src/router/index.ts +4 -1
- package/src/views/MediaBrowser.tsx +89 -67
- package/src/views/Posts/NewPostTypeInstructions.tsx +2 -2
- package/src/views/Posts/post-type-presets.ts +15 -64
- package/src/views/SEO.tsx +186 -863
- package/src/views/seo/AuditTab.tsx +229 -0
- package/src/views/seo/ContentTab.tsx +164 -0
- package/src/views/seo/OverviewTab.tsx +257 -0
- package/src/views/seo/RedirectsTab.tsx +392 -0
- package/src/views/seo/TechnicalTab.tsx +303 -0
- 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'
|
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -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
|