@actuate-media/cms-admin 0.36.0 → 0.36.1
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/actuate-admin.css +1 -1
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +14 -13
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/Profile.d.ts.map +1 -1
- package/dist/views/Profile.js +1 -1
- package/dist/views/Profile.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +25 -20
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/Users.js.map +1 -1
- package/dist/views/settings/AISettingsTab.js +40 -42
- package/dist/views/settings/AISettingsTab.js.map +1 -1
- package/dist/views/settings/AiFeaturesCard.d.ts.map +1 -1
- package/dist/views/settings/AiFeaturesCard.js +2 -3
- package/dist/views/settings/AiFeaturesCard.js.map +1 -1
- package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
- package/dist/views/settings/BrandVoiceCard.js +2 -4
- package/dist/views/settings/BrandVoiceCard.js.map +1 -1
- package/dist/views/settings/GeneralSettingsTab.js +1 -1
- package/dist/views/settings/LanguageRegionCard.d.ts.map +1 -1
- package/dist/views/settings/LanguageRegionCard.js +2 -1
- package/dist/views/settings/LanguageRegionCard.js.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.js +3 -3
- package/dist/views/settings/MonthlyUsageCard.js.map +1 -1
- package/dist/views/settings/ProviderConnectionCard.js +1 -1
- package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
- package/dist/views/settings/SecurityStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SecurityStatusCard.js +2 -3
- package/dist/views/settings/SecurityStatusCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.js +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.js +3 -4
- package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -1
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +5 -8
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.js +3 -2
- package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
- package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
- package/dist/views/settings/SiteInformationCard.js +4 -4
- package/dist/views/settings/SiteInformationCard.js.map +1 -1
- package/dist/views/settings/components.d.ts +31 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +22 -0
- package/dist/views/settings/components.js.map +1 -1
- package/dist/views/settings/securityPrimitives.d.ts +11 -3
- package/dist/views/settings/securityPrimitives.d.ts.map +1 -1
- package/dist/views/settings/securityPrimitives.js +10 -3
- package/dist/views/settings/securityPrimitives.js.map +1 -1
- package/package.json +1 -1
- package/src/views/ApiKeys.tsx +190 -154
- package/src/views/Profile.tsx +4 -2
- package/src/views/Settings.tsx +154 -143
- package/src/views/Users.tsx +1 -1
- package/src/views/settings/AISettingsTab.tsx +64 -76
- package/src/views/settings/AiFeaturesCard.tsx +6 -12
- package/src/views/settings/BrandVoiceCard.tsx +6 -19
- package/src/views/settings/GeneralSettingsTab.tsx +1 -1
- package/src/views/settings/LanguageRegionCard.tsx +5 -7
- package/src/views/settings/MonthlyUsageCard.tsx +6 -10
- package/src/views/settings/ProviderConnectionCard.tsx +1 -1
- package/src/views/settings/SecurityStatusCard.tsx +8 -11
- package/src/views/settings/SecurityTab.tsx +1 -1
- package/src/views/settings/SeoRobotsDefaultsCard.tsx +7 -14
- package/src/views/settings/SeoSettingsTab.tsx +17 -26
- package/src/views/settings/SiteIndexStatusCard.tsx +10 -22
- package/src/views/settings/SiteInformationCard.tsx +3 -9
- package/src/views/settings/components.tsx +72 -1
- package/src/views/settings/securityPrimitives.tsx +14 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { AlertTriangle,
|
|
3
|
+
import { AlertTriangle, Info, Loader2, Plus } from 'lucide-react'
|
|
4
4
|
import { useId, useMemo, useState } from 'react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { AiFeaturesCard } from './AiFeaturesCard.js'
|
|
@@ -8,7 +8,7 @@ import { BrandVoiceCard } from './BrandVoiceCard.js'
|
|
|
8
8
|
import { ModelSelector } from './ModelSelector.js'
|
|
9
9
|
import { MonthlyUsageCard } from './MonthlyUsageCard.js'
|
|
10
10
|
import { ProviderConnectionCard } from './ProviderConnectionCard.js'
|
|
11
|
-
import { SettingsSaveBar } from './components.js'
|
|
11
|
+
import { SettingsCard, SettingsSaveBar } from './components.js'
|
|
12
12
|
import {
|
|
13
13
|
useAiSettings,
|
|
14
14
|
type AiProviderKey,
|
|
@@ -41,7 +41,7 @@ export function AISettingsTab({ role }: AISettingsTabProps) {
|
|
|
41
41
|
<div className="space-y-4" aria-busy="true" aria-live="polite">
|
|
42
42
|
<span className="sr-only">Loading AI settings…</span>
|
|
43
43
|
{[0, 1, 2].map((i) => (
|
|
44
|
-
<div key={i} className="border-border bg-card rounded-lg border p-
|
|
44
|
+
<div key={i} className="border-border bg-card rounded-lg border p-6">
|
|
45
45
|
<div className="bg-muted mb-4 h-4 w-40 animate-pulse rounded" />
|
|
46
46
|
<div className="space-y-3">
|
|
47
47
|
<div className="bg-muted h-9 w-full animate-pulse rounded" />
|
|
@@ -125,7 +125,6 @@ export function AISettingsTab({ role }: AISettingsTabProps) {
|
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
|
|
127
127
|
function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean }) {
|
|
128
|
-
const headingId = useId()
|
|
129
128
|
const providerSelectId = useId()
|
|
130
129
|
const [refreshing, setRefreshing] = useState(false)
|
|
131
130
|
|
|
@@ -150,21 +149,10 @@ function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean })
|
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
return (
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
</span>
|
|
158
|
-
<div>
|
|
159
|
-
<h3 id={headingId} className="text-foreground text-base font-medium">
|
|
160
|
-
AI Assistant
|
|
161
|
-
</h3>
|
|
162
|
-
<p className="text-muted-foreground text-sm">
|
|
163
|
-
The default provider and model used across AI features.
|
|
164
|
-
</p>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
152
|
+
<SettingsCard
|
|
153
|
+
title="AI Assistant"
|
|
154
|
+
description="The default provider and model used across AI features."
|
|
155
|
+
>
|
|
168
156
|
{ai.providers.length === 0 ? (
|
|
169
157
|
<div className="border-border bg-muted/40 text-muted-foreground rounded-md border border-dashed p-4 text-sm">
|
|
170
158
|
No AI providers are connected yet. Add one below to enable AI features.
|
|
@@ -206,7 +194,7 @@ function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean })
|
|
|
206
194
|
/>
|
|
207
195
|
</div>
|
|
208
196
|
)}
|
|
209
|
-
</
|
|
197
|
+
</SettingsCard>
|
|
210
198
|
)
|
|
211
199
|
}
|
|
212
200
|
|
|
@@ -215,16 +203,14 @@ function AssistantCard({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean })
|
|
|
215
203
|
// ---------------------------------------------------------------------------
|
|
216
204
|
|
|
217
205
|
function ProviderConnectionsSection({ ai, canEdit }: { ai: UseAiSettings; canEdit: boolean }) {
|
|
218
|
-
const headingId = useId()
|
|
219
206
|
const [adding, setAdding] = useState(false)
|
|
220
207
|
|
|
221
208
|
return (
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
{canEdit && !adding && (
|
|
209
|
+
<SettingsCard
|
|
210
|
+
title="Provider connections"
|
|
211
|
+
description="Connect Anthropic, OpenAI, xAI, or a custom OpenAI-compatible endpoint."
|
|
212
|
+
actions={
|
|
213
|
+
canEdit && !adding ? (
|
|
228
214
|
<button
|
|
229
215
|
type="button"
|
|
230
216
|
onClick={() => setAdding(true)}
|
|
@@ -233,57 +219,59 @@ function ProviderConnectionsSection({ ai, canEdit }: { ai: UseAiSettings; canEdi
|
|
|
233
219
|
<Plus size={14} aria-hidden="true" />
|
|
234
220
|
Add provider
|
|
235
221
|
</button>
|
|
222
|
+
) : undefined
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
<div className="space-y-3">
|
|
226
|
+
{adding && (
|
|
227
|
+
<AddProviderForm
|
|
228
|
+
ai={ai}
|
|
229
|
+
onDone={() => setAdding(false)}
|
|
230
|
+
onCancel={() => setAdding(false)}
|
|
231
|
+
/>
|
|
236
232
|
)}
|
|
237
|
-
</div>
|
|
238
233
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}}
|
|
275
|
-
onDisconnect={async () => {
|
|
276
|
-
const res = await ai.deleteProvider(p.id)
|
|
277
|
-
if (!res.ok) toast.error(res.error ?? 'Failed to disconnect.')
|
|
278
|
-
else toast.success(`${p.displayName} disconnected.`)
|
|
279
|
-
}}
|
|
280
|
-
/>
|
|
281
|
-
))}
|
|
234
|
+
{ai.providers.map((p) => (
|
|
235
|
+
<ProviderConnectionCard
|
|
236
|
+
key={p.id}
|
|
237
|
+
provider={p}
|
|
238
|
+
canEdit={canEdit}
|
|
239
|
+
busy={ai.busyProviderId === p.id}
|
|
240
|
+
onTest={async () => {
|
|
241
|
+
const res = await ai.testProvider(p.id)
|
|
242
|
+
if (!res.ok) toast.error(res.error ?? res.message ?? 'Connection test failed.')
|
|
243
|
+
else toast.success(res.message ?? 'Connection successful.')
|
|
244
|
+
}}
|
|
245
|
+
onSyncModels={async () => {
|
|
246
|
+
const res = await ai.syncModels(p.id)
|
|
247
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to refresh models.')
|
|
248
|
+
else if (res.fromFallback)
|
|
249
|
+
toast.warning(res.warning ?? 'Showing the curated model list.')
|
|
250
|
+
else toast.success('Model list refreshed.')
|
|
251
|
+
}}
|
|
252
|
+
onUpdateKey={async (apiKey) => {
|
|
253
|
+
const res = await ai.updateProvider(p.id, { apiKey })
|
|
254
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to update key.')
|
|
255
|
+
else toast.success('API key updated.')
|
|
256
|
+
}}
|
|
257
|
+
onUpdateBaseUrl={async (baseUrl) => {
|
|
258
|
+
const res = await ai.updateProvider(p.id, { baseUrl })
|
|
259
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to update base URL.')
|
|
260
|
+
else toast.success('Base URL updated.')
|
|
261
|
+
}}
|
|
262
|
+
onDisconnect={async () => {
|
|
263
|
+
const res = await ai.deleteProvider(p.id)
|
|
264
|
+
if (!res.ok) toast.error(res.error ?? 'Failed to disconnect.')
|
|
265
|
+
else toast.success(`${p.displayName} disconnected.`)
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
))}
|
|
282
269
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
270
|
+
{ai.providers.length === 0 && !adding && (
|
|
271
|
+
<p className="text-muted-foreground text-sm">No providers connected.</p>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</SettingsCard>
|
|
287
275
|
)
|
|
288
276
|
}
|
|
289
277
|
|
|
@@ -332,7 +320,7 @@ function AddProviderForm({
|
|
|
332
320
|
}
|
|
333
321
|
|
|
334
322
|
return (
|
|
335
|
-
<div className="border-border bg-
|
|
323
|
+
<div className="border-border bg-background rounded-lg border p-4">
|
|
336
324
|
<h4 className="text-foreground mb-3 text-base font-medium">Add a provider</h4>
|
|
337
325
|
<div className="space-y-3">
|
|
338
326
|
<div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { useId } from 'react'
|
|
5
|
-
import { SettingsToggleRow } from './components.js'
|
|
5
|
+
import { SettingsCard, SettingsToggleRow } from './components.js'
|
|
6
6
|
import { capabilityLabel, effectiveFeatureModel, isModelCompatible } from './featureModel.js'
|
|
7
7
|
import type { AiFeatureView, AiModelView } from './useAiSettings.js'
|
|
8
8
|
|
|
@@ -31,17 +31,11 @@ export function AiFeaturesCard({
|
|
|
31
31
|
onToggle: (key: string, enabled: boolean) => void
|
|
32
32
|
onSetFeatureModel: (key: string, modelId: string) => void
|
|
33
33
|
}) {
|
|
34
|
-
const headingId = useId()
|
|
35
34
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<p className="text-muted-foreground mb-4 text-sm">
|
|
41
|
-
Turn individual AI-powered features on or off, and optionally route each to a specific
|
|
42
|
-
model.
|
|
43
|
-
</p>
|
|
44
|
-
|
|
35
|
+
<SettingsCard
|
|
36
|
+
title="AI Features"
|
|
37
|
+
description="Turn individual AI-powered features on or off, and optionally route each to a specific model."
|
|
38
|
+
>
|
|
45
39
|
{features.length === 0 ? (
|
|
46
40
|
<p className="text-muted-foreground text-sm">No AI features are available.</p>
|
|
47
41
|
) : (
|
|
@@ -59,7 +53,7 @@ export function AiFeaturesCard({
|
|
|
59
53
|
))}
|
|
60
54
|
</div>
|
|
61
55
|
)}
|
|
62
|
-
</
|
|
56
|
+
</SettingsCard>
|
|
63
57
|
)
|
|
64
58
|
}
|
|
65
59
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { Sparkles } from 'lucide-react'
|
|
4
3
|
import { useId } from 'react'
|
|
5
|
-
import { SettingsToggleRow } from './components.js'
|
|
4
|
+
import { SettingsCard, SettingsToggleRow } from './components.js'
|
|
6
5
|
import type { BrandVoiceForm } from './useAiSettings.js'
|
|
7
6
|
|
|
8
7
|
const INPUT_CLASS =
|
|
@@ -28,7 +27,6 @@ export function BrandVoiceCard({
|
|
|
28
27
|
canEdit: boolean
|
|
29
28
|
onChange: (patch: Partial<BrandVoiceForm>) => void
|
|
30
29
|
}) {
|
|
31
|
-
const headingId = useId()
|
|
32
30
|
const nameId = useId()
|
|
33
31
|
const toneId = useId()
|
|
34
32
|
const audienceId = useId()
|
|
@@ -37,21 +35,10 @@ export function BrandVoiceCard({
|
|
|
37
35
|
const disabled = !canEdit
|
|
38
36
|
|
|
39
37
|
return (
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</span>
|
|
45
|
-
<div>
|
|
46
|
-
<h3 id={headingId} className="text-foreground text-base font-medium">
|
|
47
|
-
Brand voice
|
|
48
|
-
</h3>
|
|
49
|
-
<p className="text-muted-foreground text-sm">
|
|
50
|
-
Teach the AI how your brand sounds. Applied to content writing and SEO copy.
|
|
51
|
-
</p>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
38
|
+
<SettingsCard
|
|
39
|
+
title="Brand voice"
|
|
40
|
+
description="Teach the AI how your brand sounds. Applied to content writing and SEO copy."
|
|
41
|
+
>
|
|
55
42
|
<div className="space-y-4">
|
|
56
43
|
<SettingsToggleRow
|
|
57
44
|
label="Apply brand voice"
|
|
@@ -147,6 +134,6 @@ export function BrandVoiceCard({
|
|
|
147
134
|
</div>
|
|
148
135
|
</div>
|
|
149
136
|
</div>
|
|
150
|
-
</
|
|
137
|
+
</SettingsCard>
|
|
151
138
|
)
|
|
152
139
|
}
|
|
@@ -25,7 +25,7 @@ export function GeneralSettingsTab({ onNavigate, role }: GeneralSettingsTabProps
|
|
|
25
25
|
<div className="space-y-4" aria-busy="true" aria-live="polite">
|
|
26
26
|
<span className="sr-only">Loading settings…</span>
|
|
27
27
|
{[0, 1, 2, 3].map((i) => (
|
|
28
|
-
<div key={i} className="border-border bg-card rounded-lg border p-
|
|
28
|
+
<div key={i} className="border-border bg-card rounded-lg border p-6">
|
|
29
29
|
<div className="bg-muted mb-4 h-4 w-40 animate-pulse rounded" />
|
|
30
30
|
<div className="space-y-3">
|
|
31
31
|
<div className="bg-muted h-9 w-full animate-pulse rounded" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useId, useMemo } from 'react'
|
|
4
|
+
import { SettingsCard } from './components.js'
|
|
4
5
|
import type { GeneralSettingsForm, ValidationIssue } from './useGeneralSettings.js'
|
|
5
6
|
|
|
6
7
|
const INPUT_CLASS =
|
|
@@ -58,13 +59,10 @@ export function LanguageRegionCard({ form, setField, issues, canEdit }: Language
|
|
|
58
59
|
}, [form.timezone])
|
|
59
60
|
|
|
60
61
|
return (
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
<SettingsCard
|
|
63
|
+
title="Language & Region"
|
|
64
|
+
description="Default language and timezone applied across the admin and public site."
|
|
64
65
|
>
|
|
65
|
-
<h3 id={`${langId}-heading`} className="text-foreground mb-4 text-base font-medium">
|
|
66
|
-
Language & Region
|
|
67
|
-
</h3>
|
|
68
66
|
<div className="space-y-4">
|
|
69
67
|
<div>
|
|
70
68
|
<label htmlFor={langId} className={LABEL_CLASS}>
|
|
@@ -114,6 +112,6 @@ export function LanguageRegionCard({ form, setField, issues, canEdit }: Language
|
|
|
114
112
|
)}
|
|
115
113
|
</div>
|
|
116
114
|
</div>
|
|
117
|
-
</
|
|
115
|
+
</SettingsCard>
|
|
118
116
|
)
|
|
119
117
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useId } from 'react'
|
|
4
|
+
import { SettingsCard } from './components.js'
|
|
4
5
|
import type { AiUsageView } from './useAiSettings.js'
|
|
5
6
|
|
|
6
7
|
const INPUT_CLASS =
|
|
@@ -31,7 +32,6 @@ export function MonthlyUsageCard({
|
|
|
31
32
|
budgetError: string | null
|
|
32
33
|
canEdit: boolean
|
|
33
34
|
}) {
|
|
34
|
-
const headingId = useId()
|
|
35
35
|
const limitId = useId()
|
|
36
36
|
|
|
37
37
|
const used = usage?.totalTokens ?? 0
|
|
@@ -44,14 +44,10 @@ export function MonthlyUsageCard({
|
|
|
44
44
|
: 'This month'
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<p className="text-muted-foreground mb-4 text-sm">
|
|
52
|
-
Token usage for {periodLabel}. Aggregated from completed AI runs.
|
|
53
|
-
</p>
|
|
54
|
-
|
|
47
|
+
<SettingsCard
|
|
48
|
+
title="Monthly Usage"
|
|
49
|
+
description={`Token usage for ${periodLabel}. Aggregated from completed AI runs.`}
|
|
50
|
+
>
|
|
55
51
|
<div className="space-y-4">
|
|
56
52
|
<div>
|
|
57
53
|
<div className="mb-1 flex items-center justify-between text-sm">
|
|
@@ -113,6 +109,6 @@ export function MonthlyUsageCard({
|
|
|
113
109
|
)}
|
|
114
110
|
</div>
|
|
115
111
|
</div>
|
|
116
|
-
</
|
|
112
|
+
</SettingsCard>
|
|
117
113
|
)
|
|
118
114
|
}
|
|
@@ -94,7 +94,7 @@ export function ProviderConnectionCard({
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
return (
|
|
97
|
-
<div className="border-border bg-
|
|
97
|
+
<div className="border-border bg-background rounded-lg border p-4">
|
|
98
98
|
<div className="mb-3 flex items-start justify-between gap-3">
|
|
99
99
|
<div>
|
|
100
100
|
<div className="flex items-center gap-2">
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useId } from 'react'
|
|
4
3
|
import { Shield, ShieldAlert, ShieldCheck, ShieldX } from 'lucide-react'
|
|
5
4
|
import type { SecurityStatus, SecurityWarning } from '@actuate-media/cms-core/security-center'
|
|
5
|
+
import { SettingsCard } from './components.js'
|
|
6
6
|
import { CheckIcon } from './securityPrimitives.js'
|
|
7
7
|
|
|
8
8
|
const LEVEL_STYLES: Record<
|
|
@@ -40,16 +40,14 @@ export function SecurityStatusCard({
|
|
|
40
40
|
status: SecurityStatus
|
|
41
41
|
onNavigate?: (path: string) => void
|
|
42
42
|
}) {
|
|
43
|
-
const headingId = useId()
|
|
44
43
|
const style = LEVEL_STYLES[status.status]
|
|
45
44
|
const { Icon } = style
|
|
46
45
|
|
|
47
46
|
return (
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</h3>
|
|
47
|
+
<SettingsCard
|
|
48
|
+
title="Security Status"
|
|
49
|
+
description={status.description}
|
|
50
|
+
actions={
|
|
53
51
|
<span
|
|
54
52
|
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-sm font-medium ${style.pill}`}
|
|
55
53
|
>
|
|
@@ -57,9 +55,8 @@ export function SecurityStatusCard({
|
|
|
57
55
|
{/* Visible text label means the status never relies on color alone. */}
|
|
58
56
|
{style.word}
|
|
59
57
|
</span>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
}
|
|
59
|
+
>
|
|
63
60
|
<div className="grid grid-cols-1 gap-x-6 gap-y-1 sm:grid-cols-2">
|
|
64
61
|
{status.checks.map((check) => (
|
|
65
62
|
<button
|
|
@@ -105,6 +102,6 @@ export function SecurityStatusCard({
|
|
|
105
102
|
))}
|
|
106
103
|
</div>
|
|
107
104
|
)}
|
|
108
|
-
</
|
|
105
|
+
</SettingsCard>
|
|
109
106
|
)
|
|
110
107
|
}
|
|
@@ -12,7 +12,7 @@ import { useSecuritySettings } from './useSecuritySettings.js'
|
|
|
12
12
|
|
|
13
13
|
function CardSkeleton() {
|
|
14
14
|
return (
|
|
15
|
-
<div className="border-border bg-card animate-pulse rounded-lg border p-
|
|
15
|
+
<div className="border-border bg-card animate-pulse rounded-lg border p-6">
|
|
16
16
|
<div className="bg-muted mb-3 h-4 w-40 rounded" />
|
|
17
17
|
<div className="bg-muted mb-4 h-3 w-72 rounded" />
|
|
18
18
|
<div className="space-y-3">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { ConfirmDangerousSettingDialog, SettingsToggleRow } from './components.js'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { ConfirmDangerousSettingDialog, SettingsCard, SettingsToggleRow } from './components.js'
|
|
5
5
|
import type { GeneralSettingsForm, SiteIndexStatus } from './useGeneralSettings.js'
|
|
6
6
|
|
|
7
7
|
interface SeoRobotsDefaultsCardProps {
|
|
@@ -29,7 +29,6 @@ export function SeoRobotsDefaultsCard({
|
|
|
29
29
|
canEdit,
|
|
30
30
|
onNavigate,
|
|
31
31
|
}: SeoRobotsDefaultsCardProps) {
|
|
32
|
-
const headingId = useId()
|
|
33
32
|
const [pending, setPending] = useState<PendingChange | null>(null)
|
|
34
33
|
|
|
35
34
|
// Some toggles can take the whole production site out of search results, so
|
|
@@ -65,16 +64,10 @@ export function SeoRobotsDefaultsCard({
|
|
|
65
64
|
const sitemapStatus = sitemapCheck ? (sitemapCheck.value === 'Yes' ? 'Enabled' : 'Disabled') : '—'
|
|
66
65
|
|
|
67
66
|
return (
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<p className="text-muted-foreground mb-4 text-sm">
|
|
73
|
-
Set the site-wide default for search engine indexing. Individual pages can inherit or
|
|
74
|
-
override these rules in their SEO panel. These share the same configuration as the SEO
|
|
75
|
-
section.
|
|
76
|
-
</p>
|
|
77
|
-
|
|
67
|
+
<SettingsCard
|
|
68
|
+
title="SEO & Robots Defaults"
|
|
69
|
+
description="Set the site-wide default for search engine indexing. Individual pages can inherit or override these rules in their SEO panel. These share the same configuration as the SEO section."
|
|
70
|
+
>
|
|
78
71
|
<div className="space-y-4">
|
|
79
72
|
<SettingsToggleRow
|
|
80
73
|
label="Default No Index"
|
|
@@ -133,7 +126,7 @@ export function SeoRobotsDefaultsCard({
|
|
|
133
126
|
}}
|
|
134
127
|
onCancel={() => setPending(null)}
|
|
135
128
|
/>
|
|
136
|
-
</
|
|
129
|
+
</SettingsCard>
|
|
137
130
|
)
|
|
138
131
|
}
|
|
139
132
|
|
|
@@ -17,6 +17,7 @@ import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
|
17
17
|
import { SEOConfigPanel } from '../../components/SEOConfigPanel.js'
|
|
18
18
|
import {
|
|
19
19
|
ConfirmDangerousSettingDialog,
|
|
20
|
+
SettingsCard,
|
|
20
21
|
SettingsSaveBar,
|
|
21
22
|
SettingsSourceBadge,
|
|
22
23
|
SettingsToggleRow,
|
|
@@ -82,7 +83,7 @@ export function SeoSettingsTab({ onNavigate, role }: SeoSettingsTabProps) {
|
|
|
82
83
|
<div className="space-y-4" aria-busy="true" aria-live="polite">
|
|
83
84
|
<span className="sr-only">Loading SEO settings…</span>
|
|
84
85
|
{[0, 1, 2, 3].map((i) => (
|
|
85
|
-
<div key={i} className="border-border bg-card rounded-lg border p-
|
|
86
|
+
<div key={i} className="border-border bg-card rounded-lg border p-6">
|
|
86
87
|
<div className="bg-muted mb-4 h-4 w-40 animate-pulse rounded" />
|
|
87
88
|
<div className="space-y-3">
|
|
88
89
|
<div className="bg-muted h-9 w-full animate-pulse rounded" />
|
|
@@ -208,7 +209,6 @@ function SeoWorkspaceCallout({ onNavigate }: { onNavigate?: (path: string) => vo
|
|
|
208
209
|
// ---------------------------------------------------------------------------
|
|
209
210
|
|
|
210
211
|
function MetaDefaultsCard({ seo, canEdit }: { seo: UseSeoSettings; canEdit: boolean }) {
|
|
211
|
-
const headingId = useId()
|
|
212
212
|
const templateId = useId()
|
|
213
213
|
const descId = useId()
|
|
214
214
|
const [pickerOpen, setPickerOpen] = useState(false)
|
|
@@ -229,11 +229,10 @@ function MetaDefaultsCard({ seo, canEdit }: { seo: UseSeoSettings; canEdit: bool
|
|
|
229
229
|
const imageEditable = sources.defaultOgImage?.editable !== false && canEdit
|
|
230
230
|
|
|
231
231
|
return (
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
232
|
+
<SettingsCard
|
|
233
|
+
title="Meta Defaults"
|
|
234
|
+
description="Fallback page title, description, and social image applied when a page defines none of its own."
|
|
235
|
+
>
|
|
237
236
|
<div className="space-y-4">
|
|
238
237
|
{/* Title template */}
|
|
239
238
|
<div>
|
|
@@ -362,7 +361,7 @@ function MetaDefaultsCard({ seo, canEdit }: { seo: UseSeoSettings; canEdit: bool
|
|
|
362
361
|
onSelect={(url) => setField('defaultOgImage', url)}
|
|
363
362
|
onSelectItem={(item) => setField('defaultSocialImageAssetId', item.id)}
|
|
364
363
|
/>
|
|
365
|
-
</
|
|
364
|
+
</SettingsCard>
|
|
366
365
|
)
|
|
367
366
|
}
|
|
368
367
|
|
|
@@ -458,20 +457,14 @@ function VerificationField({
|
|
|
458
457
|
}
|
|
459
458
|
|
|
460
459
|
function SearchEngineVerificationCard({ seo, canEdit }: { seo: UseSeoSettings; canEdit: boolean }) {
|
|
461
|
-
const headingId = useId()
|
|
462
460
|
const { form, setField } = seo
|
|
463
461
|
const onChange = (key: keyof SeoSettingsForm, value: string) => setField(key, value as never)
|
|
464
462
|
|
|
465
463
|
return (
|
|
466
|
-
<
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
<p className="text-muted-foreground mb-4 text-sm">
|
|
471
|
-
Paste the verification token from each provider. We render only safe meta tags into your
|
|
472
|
-
site's head — never arbitrary HTML.
|
|
473
|
-
</p>
|
|
474
|
-
|
|
464
|
+
<SettingsCard
|
|
465
|
+
title="Search Engine Verification"
|
|
466
|
+
description="Paste the verification token from each provider. We render only safe meta tags into your site's head — never arbitrary HTML."
|
|
467
|
+
>
|
|
475
468
|
<div className="space-y-4">
|
|
476
469
|
<VerificationField
|
|
477
470
|
label="Google Search Console"
|
|
@@ -523,7 +516,7 @@ function SearchEngineVerificationCard({ seo, canEdit }: { seo: UseSeoSettings; c
|
|
|
523
516
|
/>
|
|
524
517
|
</div>
|
|
525
518
|
</details>
|
|
526
|
-
</
|
|
519
|
+
</SettingsCard>
|
|
527
520
|
)
|
|
528
521
|
}
|
|
529
522
|
|
|
@@ -540,7 +533,6 @@ function SitemapDefaultsCard({
|
|
|
540
533
|
canEdit: boolean
|
|
541
534
|
onNavigate?: (path: string) => void
|
|
542
535
|
}) {
|
|
543
|
-
const headingId = useId()
|
|
544
536
|
const [pendingDisable, setPendingDisable] = useState(false)
|
|
545
537
|
const { form, setField, sitemap, siteUrl, isProduction, regenerateSitemap, regenerating } = seo
|
|
546
538
|
|
|
@@ -558,11 +550,10 @@ function SitemapDefaultsCard({
|
|
|
558
550
|
: 'Never'
|
|
559
551
|
|
|
560
552
|
return (
|
|
561
|
-
<
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
553
|
+
<SettingsCard
|
|
554
|
+
title="Sitemap"
|
|
555
|
+
description="Control XML sitemap generation and whether search engines are pinged on publish."
|
|
556
|
+
>
|
|
566
557
|
{!siteUrl && (
|
|
567
558
|
<div className="border-warning/30 bg-warning/10 text-warning mb-4 flex items-start gap-2 rounded-md border p-3 text-sm">
|
|
568
559
|
<AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
|
|
@@ -644,7 +635,7 @@ function SitemapDefaultsCard({
|
|
|
644
635
|
}}
|
|
645
636
|
onCancel={() => setPendingDisable(false)}
|
|
646
637
|
/>
|
|
647
|
-
</
|
|
638
|
+
</SettingsCard>
|
|
648
639
|
)
|
|
649
640
|
}
|
|
650
641
|
|