@actuate-media/cms-admin 0.3.0 → 0.4.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.js +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/layout/Sidebar.d.ts +7 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +34 -10
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +49 -5
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/Settings.d.ts +2 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +31 -3
- package/dist/views/Settings.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +1 -1
- package/src/layout/Sidebar.tsx +70 -22
- package/src/views/DocumentEdit.tsx +82 -4
- package/src/views/Settings.tsx +79 -2
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
BookOpen,
|
|
17
17
|
HelpCircle,
|
|
18
18
|
Newspaper,
|
|
19
|
+
PanelTop,
|
|
20
|
+
PanelBottom,
|
|
21
|
+
Layers,
|
|
19
22
|
} from 'lucide-react';
|
|
20
23
|
import type { LucideIcon } from 'lucide-react';
|
|
21
24
|
|
|
@@ -53,6 +56,9 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
|
|
53
56
|
book: BookOpen,
|
|
54
57
|
help: HelpCircle,
|
|
55
58
|
newspaper: Newspaper,
|
|
59
|
+
PanelTop: PanelTop,
|
|
60
|
+
PanelBottom: PanelBottom,
|
|
61
|
+
Layers: Layers,
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean }) {
|
|
@@ -143,28 +149,44 @@ export function Sidebar({ collapsed, onToggleCollapse, currentPath, onNavigate,
|
|
|
143
149
|
</div>
|
|
144
150
|
|
|
145
151
|
<nav className="p-3 space-y-1">
|
|
146
|
-
{navItems.map((item) => {
|
|
152
|
+
{navItems.map((item, idx) => {
|
|
147
153
|
const Icon = item.icon;
|
|
148
154
|
const isActive =
|
|
149
155
|
currentPath === item.path ||
|
|
150
156
|
(item.path !== '/' && currentPath.startsWith(item.path));
|
|
151
157
|
|
|
158
|
+
const prevGroup = idx > 0 ? navItems[idx - 1]?.group : undefined;
|
|
159
|
+
const showGroupLabel = item.group && item.group !== prevGroup;
|
|
160
|
+
|
|
152
161
|
return (
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<span className="text-sm font-medium">{item.label}</span>
|
|
162
|
+
<div key={item.path}>
|
|
163
|
+
{showGroupLabel && !collapsed && (
|
|
164
|
+
<div className="pt-3 pb-1 px-3">
|
|
165
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-sidebar-foreground/50">
|
|
166
|
+
{item.group}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
{showGroupLabel && collapsed && (
|
|
171
|
+
<div className="pt-2 pb-1 flex justify-center">
|
|
172
|
+
<span className="w-4 border-t border-sidebar-foreground/20" />
|
|
173
|
+
</div>
|
|
166
174
|
)}
|
|
167
|
-
|
|
175
|
+
<button
|
|
176
|
+
onClick={() => onNavigate(item.path)}
|
|
177
|
+
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors w-full text-left ${
|
|
178
|
+
isActive
|
|
179
|
+
? 'bg-sidebar-accent text-sidebar-primary'
|
|
180
|
+
: 'text-sidebar-foreground hover:bg-sidebar-accent'
|
|
181
|
+
} ${collapsed ? 'justify-center' : ''}`}
|
|
182
|
+
title={collapsed ? item.label : ''}
|
|
183
|
+
>
|
|
184
|
+
<Icon className="w-5 h-5 shrink-0" />
|
|
185
|
+
{!collapsed && (
|
|
186
|
+
<span className="text-sm font-medium">{item.label}</span>
|
|
187
|
+
)}
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
168
190
|
);
|
|
169
191
|
})}
|
|
170
192
|
</nav>
|
|
@@ -178,7 +200,14 @@ function resolveIcon(collection: any): LucideIcon {
|
|
|
178
200
|
return collection.type === 'page' ? File : FileText;
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
|
|
203
|
+
export interface NavItem {
|
|
204
|
+
path: string;
|
|
205
|
+
label: string;
|
|
206
|
+
icon: LucideIcon;
|
|
207
|
+
group?: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildNavItems(config: any): NavItem[] {
|
|
182
211
|
if (!config?.collections) return defaultNavItems;
|
|
183
212
|
|
|
184
213
|
const raw = config.collections;
|
|
@@ -186,17 +215,19 @@ function buildNavItems(config: any) {
|
|
|
186
215
|
|
|
187
216
|
const visible = collectionsList.filter((c) => !c.admin?.hidden);
|
|
188
217
|
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const other = visible.filter((c) => c.type !== 'page' && c.type !== 'post');
|
|
218
|
+
const ungrouped = visible.filter((c) => !c.admin?.group);
|
|
219
|
+
const grouped = visible.filter((c) => c.admin?.group);
|
|
192
220
|
|
|
193
|
-
const
|
|
221
|
+
const pages = ungrouped.filter((c) => c.type === 'page');
|
|
222
|
+
const posts = ungrouped.filter((c) => c.type === 'post');
|
|
223
|
+
const other = ungrouped.filter((c) => c.type !== 'page' && c.type !== 'post');
|
|
224
|
+
const sortedUngrouped = [...pages, ...posts, ...other];
|
|
194
225
|
|
|
195
|
-
const items:
|
|
226
|
+
const items: NavItem[] = [
|
|
196
227
|
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
|
|
197
228
|
];
|
|
198
229
|
|
|
199
|
-
for (const collection of
|
|
230
|
+
for (const collection of sortedUngrouped) {
|
|
200
231
|
items.push({
|
|
201
232
|
label: collection.labels?.plural ?? collection.slug,
|
|
202
233
|
path: `/${collection.slug}`,
|
|
@@ -204,6 +235,23 @@ function buildNavItems(config: any) {
|
|
|
204
235
|
});
|
|
205
236
|
}
|
|
206
237
|
|
|
238
|
+
const groups = new Map<string, typeof grouped>();
|
|
239
|
+
for (const col of grouped) {
|
|
240
|
+
const group = col.admin.group as string;
|
|
241
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
242
|
+
groups.get(group)!.push(col);
|
|
243
|
+
}
|
|
244
|
+
for (const [groupName, cols] of groups) {
|
|
245
|
+
for (const collection of cols) {
|
|
246
|
+
items.push({
|
|
247
|
+
label: collection.labels?.plural ?? collection.slug,
|
|
248
|
+
path: `/${collection.slug}`,
|
|
249
|
+
icon: resolveIcon(collection),
|
|
250
|
+
group: groupName,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
207
255
|
items.push(
|
|
208
256
|
{ path: '/media', label: 'Media', icon: Image },
|
|
209
257
|
{ path: '/forms', label: 'Forms', icon: ClipboardList },
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
4
4
|
import { AlertTriangle, Eye, EyeOff, Save, Copy, Loader2, Clock } from 'lucide-react';
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
6
|
import { FieldRenderer } from '../fields/FieldRenderer.js';
|
|
7
|
+
import { RelationshipField } from '../fields/RelationshipField.js';
|
|
7
8
|
import { Button } from '../components/ui/Button.js';
|
|
8
9
|
import { Badge } from '../components/ui/Badge.js';
|
|
9
10
|
import { LivePreview } from '../components/LivePreview.js';
|
|
@@ -26,6 +27,8 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
26
27
|
const [initialValues, setInitialValues] = useState<Record<string, any>>({});
|
|
27
28
|
const [seoData, setSeoData] = useState<SEOData>({});
|
|
28
29
|
const [initialSeoData, setInitialSeoData] = useState<SEOData>({});
|
|
30
|
+
const [layoutAssignments, setLayoutAssignments] = useState<Record<string, string>>({});
|
|
31
|
+
const [initialLayoutAssignments, setInitialLayoutAssignments] = useState<Record<string, string>>({});
|
|
29
32
|
const [saving, setSaving] = useState(false);
|
|
30
33
|
const [loading, setLoading] = useState(!isNew);
|
|
31
34
|
const [showPreview, setShowPreview] = useState(false);
|
|
@@ -38,6 +41,16 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
38
41
|
? collections.find((c: any) => c.slug === collectionSlug)
|
|
39
42
|
: collections?.[collectionSlug];
|
|
40
43
|
|
|
44
|
+
const layoutConfig = config?.layout;
|
|
45
|
+
const layoutRegions: Array<{ name: string; collection: string; label: string }> = layoutConfig?.regions
|
|
46
|
+
? Object.entries(layoutConfig.regions).map(([name, region]: [string, any]) => ({
|
|
47
|
+
name,
|
|
48
|
+
collection: region.collection,
|
|
49
|
+
label: region.label ?? name.charAt(0).toUpperCase() + name.slice(1),
|
|
50
|
+
}))
|
|
51
|
+
: [];
|
|
52
|
+
const hasLayout = layoutRegions.length > 0 && (collection?.type === 'page' || collection?.urlPrefix !== undefined);
|
|
53
|
+
|
|
41
54
|
const previewUrl = collection?.admin?.preview ? collection.admin.preview({}) : undefined;
|
|
42
55
|
const fields: any[] = collection?.fields
|
|
43
56
|
? (Array.isArray(collection.fields)
|
|
@@ -51,7 +64,8 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
51
64
|
: `Edit ${values[useAsTitleField] ?? 'Document'}`;
|
|
52
65
|
|
|
53
66
|
const isDirty = JSON.stringify(values) !== JSON.stringify(initialValues)
|
|
54
|
-
|| JSON.stringify(seoData) !== JSON.stringify(initialSeoData)
|
|
67
|
+
|| JSON.stringify(seoData) !== JSON.stringify(initialSeoData)
|
|
68
|
+
|| JSON.stringify(layoutAssignments) !== JSON.stringify(initialLayoutAssignments);
|
|
55
69
|
|
|
56
70
|
useEffect(() => {
|
|
57
71
|
if (!isNew && documentId && !hasLoadedRef.current) {
|
|
@@ -95,6 +109,12 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
95
109
|
}
|
|
96
110
|
setSeoData(loadedSeo);
|
|
97
111
|
setInitialSeoData(loadedSeo);
|
|
112
|
+
|
|
113
|
+
if (docData._layout && typeof docData._layout === 'object') {
|
|
114
|
+
const loaded = docData._layout as Record<string, string>;
|
|
115
|
+
setLayoutAssignments(loaded);
|
|
116
|
+
setInitialLayoutAssignments(loaded);
|
|
117
|
+
}
|
|
98
118
|
} else if (res.error) {
|
|
99
119
|
toast.error(`Failed to load document: ${res.error}`);
|
|
100
120
|
}
|
|
@@ -107,7 +127,8 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
107
127
|
|
|
108
128
|
async function handleSave() {
|
|
109
129
|
setSaving(true);
|
|
110
|
-
const
|
|
130
|
+
const layoutPayload = Object.keys(layoutAssignments).length > 0 ? { _layout: layoutAssignments } : {};
|
|
131
|
+
const payload = { ...values, ...seoData, ...layoutPayload };
|
|
111
132
|
try {
|
|
112
133
|
if (isNew) {
|
|
113
134
|
const res = await cmsApi<any>(`/collections/${collectionSlug}`, {
|
|
@@ -121,6 +142,7 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
121
142
|
const newId = res.data?.id;
|
|
122
143
|
setInitialValues(values);
|
|
123
144
|
setInitialSeoData(seoData);
|
|
145
|
+
setInitialLayoutAssignments(layoutAssignments);
|
|
124
146
|
if (newId && onNavigate) {
|
|
125
147
|
onNavigate(`/${collectionSlug}/${newId}`);
|
|
126
148
|
}
|
|
@@ -136,6 +158,7 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
136
158
|
toast.success('Changes saved');
|
|
137
159
|
setInitialValues(values);
|
|
138
160
|
setInitialSeoData(seoData);
|
|
161
|
+
setInitialLayoutAssignments(layoutAssignments);
|
|
139
162
|
if (res.data?.status) setDocStatus(res.data.status);
|
|
140
163
|
}
|
|
141
164
|
}
|
|
@@ -148,9 +171,10 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
148
171
|
async function handlePublish() {
|
|
149
172
|
if (isNew) return;
|
|
150
173
|
setSaving(true);
|
|
174
|
+
const layoutPayload = Object.keys(layoutAssignments).length > 0 ? { _layout: layoutAssignments } : {};
|
|
151
175
|
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`, {
|
|
152
176
|
method: 'PUT',
|
|
153
|
-
body: JSON.stringify({ ...values, ...seoData, status: 'PUBLISHED' }),
|
|
177
|
+
body: JSON.stringify({ ...values, ...seoData, ...layoutPayload, status: 'PUBLISHED' }),
|
|
154
178
|
});
|
|
155
179
|
if (res.error) {
|
|
156
180
|
toast.error(res.error);
|
|
@@ -159,6 +183,7 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
159
183
|
setDocStatus('PUBLISHED');
|
|
160
184
|
setInitialValues(values);
|
|
161
185
|
setInitialSeoData(seoData);
|
|
186
|
+
setInitialLayoutAssignments(layoutAssignments);
|
|
162
187
|
}
|
|
163
188
|
setSaving(false);
|
|
164
189
|
}
|
|
@@ -166,9 +191,10 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
166
191
|
async function handleUnpublish() {
|
|
167
192
|
if (isNew) return;
|
|
168
193
|
setSaving(true);
|
|
194
|
+
const layoutPayload = Object.keys(layoutAssignments).length > 0 ? { _layout: layoutAssignments } : {};
|
|
169
195
|
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`, {
|
|
170
196
|
method: 'PUT',
|
|
171
|
-
body: JSON.stringify({ ...values, ...seoData, status: 'DRAFT' }),
|
|
197
|
+
body: JSON.stringify({ ...values, ...seoData, ...layoutPayload, status: 'DRAFT' }),
|
|
172
198
|
});
|
|
173
199
|
if (res.error) {
|
|
174
200
|
toast.error(res.error);
|
|
@@ -177,6 +203,7 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
177
203
|
setDocStatus('DRAFT');
|
|
178
204
|
setInitialValues(values);
|
|
179
205
|
setInitialSeoData(seoData);
|
|
206
|
+
setInitialLayoutAssignments(layoutAssignments);
|
|
180
207
|
}
|
|
181
208
|
setSaving(false);
|
|
182
209
|
}
|
|
@@ -335,6 +362,57 @@ export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }:
|
|
|
335
362
|
onChange={setSeoData}
|
|
336
363
|
siteUrl={config?.siteUrl}
|
|
337
364
|
/>
|
|
365
|
+
|
|
366
|
+
{hasLayout && (
|
|
367
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
|
|
368
|
+
<h3 className="mb-1 font-semibold">Layout</h3>
|
|
369
|
+
<p className="mb-4 text-xs text-[var(--muted-foreground)]">
|
|
370
|
+
Assign header/footer variants. Child pages inherit from ancestors.
|
|
371
|
+
</p>
|
|
372
|
+
<div className="space-y-4">
|
|
373
|
+
{layoutRegions.map((region) => (
|
|
374
|
+
<div key={region.name}>
|
|
375
|
+
<RelationshipField
|
|
376
|
+
label={region.label}
|
|
377
|
+
value={layoutAssignments[region.name] ?? ''}
|
|
378
|
+
onChange={(val) => {
|
|
379
|
+
setLayoutAssignments((prev) => {
|
|
380
|
+
const next = { ...prev };
|
|
381
|
+
if (val && typeof val === 'string') {
|
|
382
|
+
next[region.name] = val;
|
|
383
|
+
} else {
|
|
384
|
+
delete next[region.name];
|
|
385
|
+
}
|
|
386
|
+
return next;
|
|
387
|
+
});
|
|
388
|
+
}}
|
|
389
|
+
relationTo={region.collection}
|
|
390
|
+
helpText={
|
|
391
|
+
!layoutAssignments[region.name]
|
|
392
|
+
? 'Inheriting from parent page or site default'
|
|
393
|
+
: undefined
|
|
394
|
+
}
|
|
395
|
+
/>
|
|
396
|
+
{layoutAssignments[region.name] && (
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
onClick={() => {
|
|
400
|
+
setLayoutAssignments((prev) => {
|
|
401
|
+
const next = { ...prev };
|
|
402
|
+
delete next[region.name];
|
|
403
|
+
return next;
|
|
404
|
+
});
|
|
405
|
+
}}
|
|
406
|
+
className="mt-1 text-xs text-[var(--primary)] hover:underline"
|
|
407
|
+
>
|
|
408
|
+
Clear override (inherit from parent)
|
|
409
|
+
</button>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
338
416
|
</div>
|
|
339
417
|
</div>
|
|
340
418
|
)}
|
package/src/views/Settings.tsx
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as Tabs from '@radix-ui/react-tabs';
|
|
4
|
-
import { Bot, Eye, EyeOff, Image, FileCode2, BookOpen, Sparkles, MessageSquare, Languages, Loader2, AlertTriangle, Download, CheckCircle2, ArrowUpCircle, ExternalLink, RefreshCw, GitPullRequest } from 'lucide-react';
|
|
4
|
+
import { Bot, Eye, EyeOff, Image, FileCode2, BookOpen, Sparkles, MessageSquare, Languages, Loader2, AlertTriangle, Download, CheckCircle2, ArrowUpCircle, ExternalLink, RefreshCw, GitPullRequest, Layers } from 'lucide-react';
|
|
5
5
|
import { useState, useEffect } from 'react';
|
|
6
6
|
import { toast } from 'sonner';
|
|
7
7
|
import { useApiData } from '../lib/useApiData.js';
|
|
8
8
|
import { cmsApi } from '../lib/api.js';
|
|
9
9
|
import { useTheme } from '../components/ThemeProvider.js';
|
|
10
|
+
import { RelationshipField } from '../fields/RelationshipField.js';
|
|
10
11
|
|
|
11
12
|
export interface SettingsProps {
|
|
12
13
|
onNavigate?: (path: string) => void;
|
|
14
|
+
config?: any;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export function Settings(_props: SettingsProps = {}) {
|
|
17
|
+
export function Settings({ config, ..._props }: SettingsProps = {}) {
|
|
16
18
|
const { data, loading, error, refetch } = useApiData<any>('/globals/settings');
|
|
17
19
|
|
|
18
20
|
const [siteTitle, setSiteTitle] = useState('My CMS');
|
|
@@ -26,6 +28,18 @@ export function Settings(_props: SettingsProps = {}) {
|
|
|
26
28
|
const [activeTab, setActiveTab] = useState('general');
|
|
27
29
|
const [saving, setSaving] = useState(false);
|
|
28
30
|
|
|
31
|
+
// Layout defaults
|
|
32
|
+
const [defaultLayout, setDefaultLayout] = useState<Record<string, string>>({});
|
|
33
|
+
const layoutConfig = config?.layout;
|
|
34
|
+
const layoutRegions: Array<{ name: string; collection: string; label: string }> = layoutConfig?.regions
|
|
35
|
+
? Object.entries(layoutConfig.regions).map(([name, region]: [string, any]) => ({
|
|
36
|
+
name,
|
|
37
|
+
collection: region.collection,
|
|
38
|
+
label: region.label ?? name.charAt(0).toUpperCase() + name.slice(1),
|
|
39
|
+
}))
|
|
40
|
+
: [];
|
|
41
|
+
const hasLayoutRegions = layoutRegions.length > 0;
|
|
42
|
+
|
|
29
43
|
// AI settings
|
|
30
44
|
const [aiProvider, setAiProvider] = useState('anthropic');
|
|
31
45
|
const [aiApiKey, setAiApiKey] = useState('');
|
|
@@ -60,11 +74,15 @@ export function Settings(_props: SettingsProps = {}) {
|
|
|
60
74
|
setAiWritingAssistant(data.aiWritingAssistant ?? true);
|
|
61
75
|
setAiContentScoring(data.aiContentScoring ?? true);
|
|
62
76
|
setAiTranslation(data.aiTranslation ?? false);
|
|
77
|
+
if (data.defaultLayout && typeof data.defaultLayout === 'object') {
|
|
78
|
+
setDefaultLayout(data.defaultLayout);
|
|
79
|
+
}
|
|
63
80
|
}
|
|
64
81
|
}, [data]);
|
|
65
82
|
|
|
66
83
|
const handleSave = async () => {
|
|
67
84
|
setSaving(true);
|
|
85
|
+
const layoutPayload = Object.keys(defaultLayout).length > 0 ? { defaultLayout } : {};
|
|
68
86
|
const res = await cmsApi('/globals/settings', {
|
|
69
87
|
method: 'PUT',
|
|
70
88
|
body: JSON.stringify({
|
|
@@ -73,6 +91,7 @@ export function Settings(_props: SettingsProps = {}) {
|
|
|
73
91
|
aiProvider, aiAltTags, aiMediaCategorize, aiMetaDescriptions,
|
|
74
92
|
aiReadability, aiSchema, aiBrandVoice, aiWritingAssistant,
|
|
75
93
|
aiContentScoring, aiTranslation,
|
|
94
|
+
...layoutPayload,
|
|
76
95
|
}),
|
|
77
96
|
});
|
|
78
97
|
setSaving(false);
|
|
@@ -112,6 +131,14 @@ export function Settings(_props: SettingsProps = {}) {
|
|
|
112
131
|
<Tabs.List className="mb-4 flex gap-1 border-b border-gray-200 overflow-x-auto">
|
|
113
132
|
<Tabs.Trigger value="general" className={tabTriggerClass}>General</Tabs.Trigger>
|
|
114
133
|
<Tabs.Trigger value="appearance" className={tabTriggerClass}>Appearance</Tabs.Trigger>
|
|
134
|
+
{hasLayoutRegions && (
|
|
135
|
+
<Tabs.Trigger value="layout" className={tabTriggerClass}>
|
|
136
|
+
<span className="flex items-center gap-1.5">
|
|
137
|
+
<Layers className="w-4 h-4" />
|
|
138
|
+
Layout
|
|
139
|
+
</span>
|
|
140
|
+
</Tabs.Trigger>
|
|
141
|
+
)}
|
|
115
142
|
<Tabs.Trigger value="security" className={tabTriggerClass}>Security</Tabs.Trigger>
|
|
116
143
|
<Tabs.Trigger value="ai" className={tabTriggerClass}>
|
|
117
144
|
<span className="flex items-center gap-1.5">
|
|
@@ -188,6 +215,56 @@ export function Settings(_props: SettingsProps = {}) {
|
|
|
188
215
|
</div>
|
|
189
216
|
</Tabs.Content>
|
|
190
217
|
|
|
218
|
+
{hasLayoutRegions && (
|
|
219
|
+
<Tabs.Content value="layout" className="space-y-4">
|
|
220
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
221
|
+
<h3 className="mb-1 text-sm font-semibold text-gray-900">Default Layout Variants</h3>
|
|
222
|
+
<p className="mb-4 text-xs text-gray-500">
|
|
223
|
+
Select the default header, footer, and other layout variants used site-wide. Pages can override these individually or inherit from parent pages.
|
|
224
|
+
</p>
|
|
225
|
+
<div className="space-y-4">
|
|
226
|
+
{layoutRegions.map((region) => (
|
|
227
|
+
<RelationshipField
|
|
228
|
+
key={region.name}
|
|
229
|
+
label={`Default ${region.label}`}
|
|
230
|
+
value={defaultLayout[region.name] ?? ''}
|
|
231
|
+
onChange={(val) => {
|
|
232
|
+
setDefaultLayout((prev) => {
|
|
233
|
+
const next = { ...prev };
|
|
234
|
+
if (val && typeof val === 'string') {
|
|
235
|
+
next[region.name] = val;
|
|
236
|
+
} else {
|
|
237
|
+
delete next[region.name];
|
|
238
|
+
}
|
|
239
|
+
return next;
|
|
240
|
+
});
|
|
241
|
+
}}
|
|
242
|
+
relationTo={region.collection}
|
|
243
|
+
helpText={`The ${region.label.toLowerCase()} variant used when no page in the ancestor chain specifies one`}
|
|
244
|
+
/>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
249
|
+
<h3 className="text-sm font-semibold text-gray-700 mb-2">How Layout Inheritance Works</h3>
|
|
250
|
+
<ul className="space-y-1.5 text-xs text-gray-600">
|
|
251
|
+
<li className="flex items-start gap-2">
|
|
252
|
+
<span className="w-4 h-4 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5">1</span>
|
|
253
|
+
<span>Each page can assign specific layout variants (header, footer, etc.) from the document editor.</span>
|
|
254
|
+
</li>
|
|
255
|
+
<li className="flex items-start gap-2">
|
|
256
|
+
<span className="w-4 h-4 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5">2</span>
|
|
257
|
+
<span>Child pages automatically inherit their parent's layout. For example, <code className="font-mono bg-gray-200 px-1 rounded">/hampton-roads/thank-you</code> inherits from <code className="font-mono bg-gray-200 px-1 rounded">/hampton-roads</code>.</span>
|
|
258
|
+
</li>
|
|
259
|
+
<li className="flex items-start gap-2">
|
|
260
|
+
<span className="w-4 h-4 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-[10px] font-bold shrink-0 mt-0.5">3</span>
|
|
261
|
+
<span>If no page in the ancestor chain sets a variant, the defaults configured above are used.</span>
|
|
262
|
+
</li>
|
|
263
|
+
</ul>
|
|
264
|
+
</div>
|
|
265
|
+
</Tabs.Content>
|
|
266
|
+
)}
|
|
267
|
+
|
|
191
268
|
<Tabs.Content value="security" className="space-y-4">
|
|
192
269
|
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
193
270
|
<h3 className="mb-4 text-sm font-semibold text-gray-900">Security Settings</h3>
|