@actuate-media/cms-admin 0.36.0 → 0.37.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/__tests__/views/appearance-settings.render.test.js +4 -4
- package/dist/__tests__/views/updates-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/updates-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/updates-tab.render.test.js +132 -0
- package/dist/__tests__/views/updates-tab.render.test.js.map +1 -0
- 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 +16 -119
- 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/AdminThemeCard.js +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/BrandPreviewCard.js +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/BrandingCard.d.ts.map +1 -1
- package/dist/views/settings/BrandingCard.js +34 -19
- package/dist/views/settings/BrandingCard.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/TypographyMotionCard.js +1 -1
- package/dist/views/settings/UpdatesTab.d.ts +6 -0
- package/dist/views/settings/UpdatesTab.d.ts.map +1 -0
- package/dist/views/settings/UpdatesTab.js +80 -0
- package/dist/views/settings/UpdatesTab.js.map +1 -0
- package/dist/views/settings/components.d.ts +51 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +33 -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/dist/views/settings/useUpdates.d.ts +67 -0
- package/dist/views/settings/useUpdates.d.ts.map +1 -0
- package/dist/views/settings/useUpdates.js +127 -0
- package/dist/views/settings/useUpdates.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/views/appearance-settings.render.test.tsx +4 -4
- package/src/__tests__/views/updates-tab.render.test.tsx +161 -0
- package/src/views/ApiKeys.tsx +190 -154
- package/src/views/Profile.tsx +4 -2
- package/src/views/Settings.tsx +26 -451
- package/src/views/Users.tsx +1 -1
- package/src/views/settings/AISettingsTab.tsx +64 -76
- package/src/views/settings/AdminThemeCard.tsx +1 -1
- package/src/views/settings/AiFeaturesCard.tsx +6 -12
- package/src/views/settings/BrandPreviewCard.tsx +1 -1
- package/src/views/settings/BrandVoiceCard.tsx +6 -19
- package/src/views/settings/BrandingCard.tsx +179 -180
- 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/TypographyMotionCard.tsx +1 -1
- package/src/views/settings/UpdatesTab.tsx +545 -0
- package/src/views/settings/components.tsx +123 -1
- package/src/views/settings/securityPrimitives.tsx +14 -9
- package/src/views/settings/useUpdates.ts +183 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from 'lucide-react'
|
|
12
12
|
import { useRef } from 'react'
|
|
13
13
|
|
|
14
|
+
import { SettingsCard, SettingsControlRow, SettingsSubsection } from './components.js'
|
|
14
15
|
import type { BrandAssetField, UseAppearanceSettings } from './useAppearanceSettings.js'
|
|
15
16
|
|
|
16
17
|
const LOGO_ACCEPT = 'image/svg+xml,image/png'
|
|
@@ -54,82 +55,131 @@ function LogoField({
|
|
|
54
55
|
const uploading = uploadingField === field
|
|
55
56
|
const inputId = `brand-${field}`
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
<div
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
const thumbnail = (
|
|
59
|
+
<div
|
|
60
|
+
className={`flex shrink-0 items-center justify-center overflow-hidden rounded-md border p-2 ${previewSurface(
|
|
61
|
+
Boolean(darkSurface),
|
|
62
|
+
)} ${square ? 'h-12 w-12' : 'h-12 w-20'}`}
|
|
63
|
+
aria-hidden={!asset}
|
|
64
|
+
>
|
|
65
|
+
{asset?.url ? (
|
|
66
|
+
// Never stretch/crop — contain preserves aspect ratio + transparency.
|
|
67
|
+
<img
|
|
68
|
+
src={asset.url}
|
|
69
|
+
alt={`${label} preview`}
|
|
70
|
+
className="max-h-full max-w-full object-contain"
|
|
71
|
+
/>
|
|
72
|
+
) : (
|
|
73
|
+
<ImageIcon size={18} className="text-muted-foreground" aria-hidden="true" />
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const controls = (
|
|
79
|
+
<>
|
|
80
|
+
<input
|
|
81
|
+
ref={inputRef}
|
|
82
|
+
id={inputId}
|
|
83
|
+
type="file"
|
|
84
|
+
accept={accept}
|
|
85
|
+
aria-label={label}
|
|
86
|
+
className="sr-only"
|
|
87
|
+
disabled={!canEdit || uploading}
|
|
88
|
+
onChange={(e) => {
|
|
89
|
+
const file = e.target.files?.[0]
|
|
90
|
+
if (file) onUpload(field, file)
|
|
91
|
+
if (inputRef.current) inputRef.current.value = ''
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
{asset && (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => onRemove(field)}
|
|
98
|
+
disabled={!canEdit}
|
|
99
|
+
className="text-muted-foreground hover:text-destructive inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
100
|
+
aria-label={`Remove ${label.toLowerCase()}`}
|
|
101
|
+
>
|
|
102
|
+
<Trash2 size={14} aria-hidden="true" /> Remove
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => inputRef.current?.click()}
|
|
108
|
+
disabled={!canEdit || uploading}
|
|
109
|
+
aria-label={`${asset ? 'Replace' : 'Upload'} ${label.toLowerCase()}`}
|
|
110
|
+
className="border-border text-foreground hover:bg-accent inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
64
111
|
>
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
<img
|
|
68
|
-
src={asset.url}
|
|
69
|
-
alt={`${label} preview`}
|
|
70
|
-
className="max-h-full max-w-full object-contain"
|
|
71
|
-
/>
|
|
112
|
+
{uploading ? (
|
|
113
|
+
<Loader2 size={14} className="animate-spin" aria-hidden="true" />
|
|
72
114
|
) : (
|
|
73
|
-
<
|
|
115
|
+
<Upload size={14} aria-hidden="true" />
|
|
74
116
|
)}
|
|
75
|
-
|
|
117
|
+
{asset ? 'Replace' : 'Upload'}
|
|
118
|
+
</button>
|
|
119
|
+
</>
|
|
120
|
+
)
|
|
76
121
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
122
|
+
return (
|
|
123
|
+
<div>
|
|
124
|
+
<SettingsControlRow
|
|
125
|
+
leading={thumbnail}
|
|
126
|
+
label={label}
|
|
127
|
+
description={helper}
|
|
128
|
+
control={controls}
|
|
129
|
+
/>
|
|
130
|
+
{warning && (
|
|
131
|
+
<p
|
|
132
|
+
className="text-warning mt-2 flex items-start gap-1.5 text-sm"
|
|
133
|
+
role="status"
|
|
134
|
+
aria-live="polite"
|
|
135
|
+
>
|
|
136
|
+
<AlertTriangle size={14} className="mt-0.5 shrink-0" aria-hidden="true" /> {warning}
|
|
137
|
+
</p>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
82
142
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
function AdminBrandSourceToggle({
|
|
144
|
+
mode,
|
|
145
|
+
canEdit,
|
|
146
|
+
onChange,
|
|
147
|
+
}: {
|
|
148
|
+
mode: UseAppearanceSettings['brand']['mode']
|
|
149
|
+
canEdit: boolean
|
|
150
|
+
onChange: (mode: 'inheritPublicBrand' | 'customAdminBrand') => void
|
|
151
|
+
}) {
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
role="radiogroup"
|
|
155
|
+
aria-label="Brand source"
|
|
156
|
+
className="border-border bg-muted inline-flex rounded-md border p-0.5"
|
|
157
|
+
>
|
|
158
|
+
{(
|
|
159
|
+
[
|
|
160
|
+
['inheritPublicBrand', 'Inherit public brand'],
|
|
161
|
+
['customAdminBrand', 'Custom admin logo'],
|
|
162
|
+
] as const
|
|
163
|
+
).map(([value, text]) => {
|
|
164
|
+
const active = mode === value
|
|
165
|
+
return (
|
|
97
166
|
<button
|
|
167
|
+
key={value}
|
|
98
168
|
type="button"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
169
|
+
role="radio"
|
|
170
|
+
aria-checked={active}
|
|
171
|
+
disabled={!canEdit}
|
|
172
|
+
onClick={() => onChange(value)}
|
|
173
|
+
className={`rounded px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
174
|
+
active
|
|
175
|
+
? 'bg-card text-foreground shadow-sm'
|
|
176
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
177
|
+
}`}
|
|
102
178
|
>
|
|
103
|
-
{
|
|
104
|
-
<Loader2 size={14} className="animate-spin" aria-hidden="true" />
|
|
105
|
-
) : (
|
|
106
|
-
<Upload size={14} aria-hidden="true" />
|
|
107
|
-
)}
|
|
108
|
-
{asset ? 'Replace' : `Upload ${label.toLowerCase()}`}
|
|
179
|
+
{text}
|
|
109
180
|
</button>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
type="button"
|
|
113
|
-
onClick={() => onRemove(field)}
|
|
114
|
-
disabled={!canEdit}
|
|
115
|
-
className="text-muted-foreground hover:text-destructive inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
116
|
-
aria-label={`Remove ${label}`}
|
|
117
|
-
>
|
|
118
|
-
<Trash2 size={14} aria-hidden="true" /> Remove
|
|
119
|
-
</button>
|
|
120
|
-
)}
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
{warning && (
|
|
124
|
-
<p
|
|
125
|
-
className="text-warning mt-2 flex items-start gap-1.5 text-sm"
|
|
126
|
-
role="status"
|
|
127
|
-
aria-live="polite"
|
|
128
|
-
>
|
|
129
|
-
<AlertTriangle size={14} className="mt-0.5 shrink-0" aria-hidden="true" /> {warning}
|
|
130
|
-
</p>
|
|
131
|
-
)}
|
|
132
|
-
</div>
|
|
181
|
+
)
|
|
182
|
+
})}
|
|
133
183
|
</div>
|
|
134
184
|
)
|
|
135
185
|
}
|
|
@@ -140,46 +190,40 @@ export function BrandingCard({ s, canEdit }: { s: UseAppearanceSettings; canEdit
|
|
|
140
190
|
|
|
141
191
|
const custom = s.brand.mode === 'customAdminBrand'
|
|
142
192
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
193
|
+
const logoFieldProps = {
|
|
194
|
+
accept: LOGO_ACCEPT,
|
|
195
|
+
brand: s.brand,
|
|
196
|
+
assets: s.assets,
|
|
197
|
+
uploadingField: s.uploadingField,
|
|
198
|
+
canEdit,
|
|
199
|
+
onUpload: s.uploadAsset,
|
|
200
|
+
onRemove: s.removeAsset,
|
|
201
|
+
}
|
|
151
202
|
|
|
203
|
+
return (
|
|
204
|
+
<SettingsCard
|
|
205
|
+
title="Branding"
|
|
206
|
+
description="Logos shown in the admin sidebar and on the public site."
|
|
207
|
+
>
|
|
152
208
|
<div className="space-y-5">
|
|
153
209
|
<LogoField
|
|
154
|
-
label="Primary
|
|
210
|
+
label="Primary logo"
|
|
155
211
|
helper="SVG or PNG, up to 1MB. Best for the public header and full admin sidebar."
|
|
156
212
|
field="publicPrimaryLogoAssetId"
|
|
157
|
-
accept={LOGO_ACCEPT}
|
|
158
|
-
brand={s.brand}
|
|
159
|
-
assets={s.assets}
|
|
160
|
-
uploadingField={s.uploadingField}
|
|
161
|
-
canEdit={canEdit}
|
|
162
213
|
warning={warningFor('publicPrimaryLogoAssetId')}
|
|
163
|
-
|
|
164
|
-
onRemove={s.removeAsset}
|
|
214
|
+
{...logoFieldProps}
|
|
165
215
|
/>
|
|
166
216
|
|
|
167
217
|
<LogoField
|
|
168
|
-
label="Favicon /
|
|
218
|
+
label="Favicon / app icon source"
|
|
169
219
|
helper="32×32 PNG or SVG. A larger square image is fine — it scales down cleanly."
|
|
170
220
|
field="faviconSourceAssetId"
|
|
171
|
-
accept={LOGO_ACCEPT}
|
|
172
221
|
square
|
|
173
|
-
brand={s.brand}
|
|
174
|
-
assets={s.assets}
|
|
175
|
-
uploadingField={s.uploadingField}
|
|
176
|
-
canEdit={canEdit}
|
|
177
222
|
warning={warningFor('faviconSourceAssetId')}
|
|
178
|
-
|
|
179
|
-
onRemove={s.removeAsset}
|
|
223
|
+
{...logoFieldProps}
|
|
180
224
|
/>
|
|
181
225
|
|
|
182
|
-
{/* Advanced logo variants disclosure (Logo
|
|
226
|
+
{/* Advanced logo variants disclosure (Logo mark + Dark mode logo). */}
|
|
183
227
|
<div className="border-border border-t pt-4">
|
|
184
228
|
<button
|
|
185
229
|
type="button"
|
|
@@ -198,109 +242,64 @@ export function BrandingCard({ s, canEdit }: { s: UseAppearanceSettings; canEdit
|
|
|
198
242
|
{s.showAdvanced && (
|
|
199
243
|
<div className="mt-4 space-y-5">
|
|
200
244
|
<LogoField
|
|
201
|
-
label="Logo
|
|
245
|
+
label="Logo mark"
|
|
202
246
|
helper="Square icon or mark. Used for the collapsed sidebar, compact UI, and app icons."
|
|
203
247
|
field="publicLogoMarkAssetId"
|
|
204
|
-
accept={LOGO_ACCEPT}
|
|
205
248
|
square
|
|
206
|
-
brand={s.brand}
|
|
207
|
-
assets={s.assets}
|
|
208
|
-
uploadingField={s.uploadingField}
|
|
209
|
-
canEdit={canEdit}
|
|
210
249
|
warning={warningFor('publicLogoMarkAssetId')}
|
|
211
|
-
|
|
212
|
-
onRemove={s.removeAsset}
|
|
250
|
+
{...logoFieldProps}
|
|
213
251
|
/>
|
|
214
252
|
<LogoField
|
|
215
|
-
label="Dark
|
|
253
|
+
label="Dark mode logo"
|
|
216
254
|
helper="Optional. Used on dark backgrounds or the dark admin theme."
|
|
217
255
|
field="publicDarkLogoAssetId"
|
|
218
|
-
accept={LOGO_ACCEPT}
|
|
219
256
|
darkSurface
|
|
220
|
-
brand={s.brand}
|
|
221
|
-
assets={s.assets}
|
|
222
|
-
uploadingField={s.uploadingField}
|
|
223
|
-
canEdit={canEdit}
|
|
224
257
|
warning={warningFor('publicDarkLogoAssetId')}
|
|
225
|
-
|
|
226
|
-
onRemove={s.removeAsset}
|
|
258
|
+
{...logoFieldProps}
|
|
227
259
|
/>
|
|
228
260
|
</div>
|
|
229
261
|
)}
|
|
230
262
|
</div>
|
|
231
263
|
|
|
232
|
-
{/* Admin branding
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
] as const
|
|
250
|
-
).map(([value, text]) => {
|
|
251
|
-
const active = s.brand.mode === value
|
|
252
|
-
return (
|
|
253
|
-
<button
|
|
254
|
-
key={value}
|
|
255
|
-
type="button"
|
|
256
|
-
role="radio"
|
|
257
|
-
aria-checked={active}
|
|
258
|
-
disabled={!canEdit}
|
|
259
|
-
onClick={() => s.setBrandingMode(value)}
|
|
260
|
-
className={`rounded px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
261
|
-
active
|
|
262
|
-
? 'bg-card text-foreground shadow-sm'
|
|
263
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
264
|
-
}`}
|
|
265
|
-
>
|
|
266
|
-
{text}
|
|
267
|
-
</button>
|
|
268
|
-
)
|
|
269
|
-
})}
|
|
270
|
-
</div>
|
|
264
|
+
{/* Admin branding — admin inherits the public brand by default. */}
|
|
265
|
+
<SettingsSubsection
|
|
266
|
+
title="Admin branding"
|
|
267
|
+
description="By default the admin reuses your public brand. Override it for a distinct internal look."
|
|
268
|
+
>
|
|
269
|
+
<div className="mt-4 space-y-5">
|
|
270
|
+
<SettingsControlRow
|
|
271
|
+
label="Brand source"
|
|
272
|
+
description="Choose whether the admin inherits the public brand or uses a custom logo."
|
|
273
|
+
control={
|
|
274
|
+
<AdminBrandSourceToggle
|
|
275
|
+
mode={s.brand.mode}
|
|
276
|
+
canEdit={canEdit}
|
|
277
|
+
onChange={s.setBrandingMode}
|
|
278
|
+
/>
|
|
279
|
+
}
|
|
280
|
+
/>
|
|
271
281
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
brand={s.brand}
|
|
294
|
-
assets={s.assets}
|
|
295
|
-
uploadingField={s.uploadingField}
|
|
296
|
-
canEdit={canEdit}
|
|
297
|
-
warning={warningFor('adminLogoMarkAssetId')}
|
|
298
|
-
onUpload={s.uploadAsset}
|
|
299
|
-
onRemove={s.removeAsset}
|
|
300
|
-
/>
|
|
301
|
-
</div>
|
|
302
|
-
)}
|
|
303
|
-
</div>
|
|
282
|
+
{custom && (
|
|
283
|
+
<div className="space-y-5">
|
|
284
|
+
<LogoField
|
|
285
|
+
label="Admin primary logo"
|
|
286
|
+
helper="Shown in the admin sidebar instead of the public logo."
|
|
287
|
+
field="adminPrimaryLogoAssetId"
|
|
288
|
+
warning={warningFor('adminPrimaryLogoAssetId')}
|
|
289
|
+
{...logoFieldProps}
|
|
290
|
+
/>
|
|
291
|
+
<LogoField
|
|
292
|
+
label="Admin logo mark"
|
|
293
|
+
helper="Square mark for the collapsed admin sidebar."
|
|
294
|
+
field="adminLogoMarkAssetId"
|
|
295
|
+
square
|
|
296
|
+
warning={warningFor('adminLogoMarkAssetId')}
|
|
297
|
+
{...logoFieldProps}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
</SettingsSubsection>
|
|
304
303
|
|
|
305
304
|
{/* Default alt text */}
|
|
306
305
|
<div className="border-border border-t pt-4">
|
|
@@ -327,6 +326,6 @@ export function BrandingCard({ s, canEdit }: { s: UseAppearanceSettings; canEdit
|
|
|
327
326
|
{s.saveError}
|
|
328
327
|
</p>
|
|
329
328
|
)}
|
|
330
|
-
</
|
|
329
|
+
</SettingsCard>
|
|
331
330
|
)
|
|
332
331
|
}
|
|
@@ -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
|
|