@prenta/admin 0.95.0 → 0.96.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/CHANGELOG.md +36 -0
- package/dist/__tests__/styles/responsive-layer.test.js +2 -2
- package/dist/__tests__/styles/responsive-layer.test.js.map +1 -1
- package/dist/__tests__/views/canvas-surface-shared.test.d.ts +2 -0
- package/dist/__tests__/views/canvas-surface-shared.test.d.ts.map +1 -0
- package/dist/__tests__/views/canvas-surface-shared.test.js +53 -0
- package/dist/__tests__/views/canvas-surface-shared.test.js.map +1 -0
- package/dist/__tests__/views/page-section-editor.test.js +39 -1
- package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +56 -1
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/CanvasSurface.d.ts +55 -0
- package/dist/views/page-editor/CanvasSurface.d.ts.map +1 -0
- package/dist/views/page-editor/CanvasSurface.js +94 -0
- package/dist/views/page-editor/CanvasSurface.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +5 -28
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +17 -79
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +14 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +16 -5
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +16 -47
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +86 -23
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/styles/responsive-layer.test.ts +2 -2
- package/src/__tests__/views/canvas-surface-shared.test.ts +61 -0
- package/src/__tests__/views/page-section-editor.test.tsx +58 -1
- package/src/__tests__/views/post-section-editor.test.tsx +91 -1
- package/src/views/page-editor/CanvasSurface.tsx +171 -0
- package/src/views/page-editor/EditorCanvas.tsx +46 -144
- package/src/views/page-editor/PageSectionEditor.tsx +13 -1
- package/src/views/post-editor/PostEditorCanvas.tsx +54 -74
- package/src/views/post-editor/PostSectionEditor.tsx +184 -82
- package/dist/components/EditorSeoAside.d.ts +0 -23
- package/dist/components/EditorSeoAside.d.ts.map +0 -1
- package/dist/components/EditorSeoAside.js +0 -21
- package/dist/components/EditorSeoAside.js.map +0 -1
- package/src/components/EditorSeoAside.tsx +0 -68
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
4
3
|
import { FileText, Plus } from 'lucide-react'
|
|
5
4
|
import { CanvasEditTip } from '../../components/CanvasEditTip.js'
|
|
5
|
+
import type { Zoom } from '../../lib/breakpoints.js'
|
|
6
6
|
import type { PageSection, PostHeaderConfig } from '../../lib/post-editor-service.js'
|
|
7
|
+
import { CanvasScroll, CanvasSurface, useSectionRefs } from '../page-editor/CanvasSurface.js'
|
|
7
8
|
import { SectionRenderer, type PostRenderContext } from '../page-editor/sections/index.js'
|
|
8
9
|
import { StructureNotice } from '../page-editor/StructureNotice.js'
|
|
9
|
-
import {
|
|
10
|
+
import { type ViewportId } from '../page-editor/viewports.js'
|
|
10
11
|
import { PostHeader } from './PostHeader.js'
|
|
11
12
|
|
|
12
13
|
interface PostEditorCanvasProps {
|
|
@@ -15,7 +16,10 @@ interface PostEditorCanvasProps {
|
|
|
15
16
|
context: PostRenderContext
|
|
16
17
|
selectedId: string | null
|
|
17
18
|
viewport: ViewportId
|
|
19
|
+
zoom?: Zoom
|
|
18
20
|
onSelect: (id: string) => void
|
|
21
|
+
onScaleChange?: (scale: number) => void
|
|
22
|
+
onCanvasElement?: (el: HTMLElement | null) => void
|
|
19
23
|
/** Add-section CTA on the empty canvas. Omitted by design-owned editors. */
|
|
20
24
|
onAddSection?: () => void
|
|
21
25
|
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
@@ -25,10 +29,17 @@ interface PostEditorCanvasProps {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/**
|
|
28
|
-
* Post preview canvas.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* Post preview canvas.
|
|
33
|
+
*
|
|
34
|
+
* Shares {@link CanvasSurface} with the page editor, so measurement, scaling,
|
|
35
|
+
* the fit box and the meta line are one implementation rather than two. What is
|
|
36
|
+
* post-specific is what goes inside: the post header renders above the body
|
|
37
|
+
* sections, so the editor shows the full layout — header regions plus sections
|
|
38
|
+
* — exactly as it will ship.
|
|
39
|
+
*
|
|
40
|
+
* Before this shared surface the post canvas derived its width from
|
|
41
|
+
* `container.clientWidth - 48`, which disagrees with the canvas's own metrics
|
|
42
|
+
* once a scrollbar appears, and it had no breakpoint/scale readout at all.
|
|
32
43
|
*/
|
|
33
44
|
export function PostEditorCanvas({
|
|
34
45
|
sections,
|
|
@@ -36,82 +47,51 @@ export function PostEditorCanvas({
|
|
|
36
47
|
context,
|
|
37
48
|
selectedId,
|
|
38
49
|
viewport,
|
|
50
|
+
zoom = 'fit',
|
|
39
51
|
onSelect,
|
|
52
|
+
onScaleChange,
|
|
53
|
+
onCanvasElement,
|
|
40
54
|
onAddSection,
|
|
41
55
|
structuralTypes = [],
|
|
42
56
|
sitePreviewAvailable = false,
|
|
43
57
|
}: PostEditorCanvasProps) {
|
|
44
|
-
const
|
|
45
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
46
|
-
const surfaceRef = useRef<HTMLDivElement>(null)
|
|
47
|
-
const sectionRefs = useRef<Map<string, HTMLElement>>(new Map())
|
|
48
|
-
const [scale, setScale] = useState(1)
|
|
49
|
-
const [surfaceHeight, setSurfaceHeight] = useState<number | null>(null)
|
|
50
|
-
|
|
51
|
-
const registerRef = useCallback((id: string, el: HTMLElement | null) => {
|
|
52
|
-
if (el) sectionRefs.current.set(id, el)
|
|
53
|
-
else sectionRefs.current.delete(id)
|
|
54
|
-
}, [])
|
|
55
|
-
|
|
56
|
-
useLayoutEffect(() => {
|
|
57
|
-
const container = containerRef.current
|
|
58
|
-
if (!container) return
|
|
59
|
-
const recompute = () => {
|
|
60
|
-
const available = container.clientWidth - 48
|
|
61
|
-
const next = Math.min(1, available / vp.width)
|
|
62
|
-
setScale(next > 0 ? next : 1)
|
|
63
|
-
if (surfaceRef.current) {
|
|
64
|
-
setSurfaceHeight(surfaceRef.current.offsetHeight * (next > 0 ? next : 1))
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
recompute()
|
|
68
|
-
const ro = new ResizeObserver(recompute)
|
|
69
|
-
ro.observe(container)
|
|
70
|
-
if (surfaceRef.current) ro.observe(surfaceRef.current)
|
|
71
|
-
return () => ro.disconnect()
|
|
72
|
-
}, [vp.width, sections, header, context])
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (!selectedId) return
|
|
76
|
-
const el = sectionRefs.current.get(selectedId)
|
|
77
|
-
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
78
|
-
}, [selectedId])
|
|
58
|
+
const registerRef = useSectionRefs(selectedId)
|
|
79
59
|
|
|
80
60
|
return (
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
<CanvasScroll>
|
|
62
|
+
<CanvasSurface
|
|
63
|
+
breakpoint={viewport}
|
|
64
|
+
zoom={zoom}
|
|
65
|
+
onScaleChange={onScaleChange}
|
|
66
|
+
onCanvasElement={onCanvasElement}
|
|
67
|
+
chrome={
|
|
68
|
+
<>
|
|
69
|
+
{sections.length > 0 && <CanvasEditTip />}
|
|
70
|
+
<StructureNotice
|
|
71
|
+
structuralTypes={structuralTypes}
|
|
72
|
+
sitePreviewAvailable={sitePreviewAvailable}
|
|
73
|
+
/>
|
|
74
|
+
</>
|
|
75
|
+
}
|
|
90
76
|
>
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
/>
|
|
110
|
-
))
|
|
111
|
-
)}
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
77
|
+
<PostHeader config={header} context={context} />
|
|
78
|
+
{sections.length === 0 ? (
|
|
79
|
+
<EmptyBody onAddSection={onAddSection} />
|
|
80
|
+
) : (
|
|
81
|
+
sections.map((section) => (
|
|
82
|
+
<SectionRenderer
|
|
83
|
+
key={section.id}
|
|
84
|
+
section={section}
|
|
85
|
+
context={context}
|
|
86
|
+
editable
|
|
87
|
+
selected={section.id === selectedId}
|
|
88
|
+
onSelect={onSelect}
|
|
89
|
+
registerRef={registerRef}
|
|
90
|
+
/>
|
|
91
|
+
))
|
|
92
|
+
)}
|
|
93
|
+
</CanvasSurface>
|
|
94
|
+
</CanvasScroll>
|
|
115
95
|
)
|
|
116
96
|
}
|
|
117
97
|
|
|
@@ -6,7 +6,9 @@ import { toast } from 'sonner'
|
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
7
7
|
import { useNavigationBlocker } from '../../router/index.js'
|
|
8
8
|
import { SchedulePublishDialog } from '../../components/SchedulePublishDialog.js'
|
|
9
|
-
import {
|
|
9
|
+
import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
10
|
+
import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
|
|
11
|
+
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
|
|
10
12
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
11
13
|
import { VersionHistory } from '../../components/VersionHistory.js'
|
|
12
14
|
import {
|
|
@@ -42,11 +44,14 @@ import { DEFAULT_VIEWPORT, type ViewportId } from '../page-editor/viewports.js'
|
|
|
42
44
|
import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
|
|
43
45
|
import { SitePreviewFrame } from '../page-editor/SitePreviewFrame.js'
|
|
44
46
|
import { useSitePreview } from '../page-editor/useSitePreview.js'
|
|
45
|
-
import {
|
|
47
|
+
import { SectionsList } from '../page-editor/SectionsPanel.js'
|
|
46
48
|
import { SectionEditForm } from '../page-editor/SectionEditForm.js'
|
|
47
49
|
import { ValidationSummary } from '../page-editor/ValidationSummary.js'
|
|
48
50
|
import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
|
|
49
|
-
import { computeLiveSeoScore } from '../../lib/editor-seo-content.js'
|
|
51
|
+
import { buildEditorContentHtml, computeLiveSeoScore } from '../../lib/editor-seo-content.js'
|
|
52
|
+
import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
|
|
53
|
+
import { ReflowContext } from '../page-editor/reflow-context.js'
|
|
54
|
+
import type { Zoom } from '../../lib/breakpoints.js'
|
|
50
55
|
import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
|
|
51
56
|
import { PostEditorCanvas } from './PostEditorCanvas.js'
|
|
52
57
|
import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
|
|
@@ -124,18 +129,45 @@ export function PostSectionEditor({
|
|
|
124
129
|
fieldPath: string
|
|
125
130
|
} | null>(null)
|
|
126
131
|
const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
|
|
132
|
+
const [zoom, setZoom] = useState<Zoom>('fit')
|
|
133
|
+
/** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
|
|
134
|
+
const [canvasScale, setCanvasScale] = useState(1)
|
|
135
|
+
/** The live canvas surface, for the reflow guard to clone. */
|
|
136
|
+
const [canvasEl, setCanvasEl] = useState<HTMLElement | null>(null)
|
|
137
|
+
/** Section content as it is on the live site, for the reflow guard's baseline. */
|
|
138
|
+
const [baseline, setBaseline] = useState<ReflowBaseline | null>(null)
|
|
127
139
|
const [dirty, setDirty] = useState(false)
|
|
128
140
|
const [saveState, setSaveState] = useState<SaveState>('idle')
|
|
129
141
|
const [publishing, setPublishing] = useState(false)
|
|
130
142
|
|
|
131
143
|
const [fieldsOpen, setFieldsOpen] = useState(false)
|
|
132
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Active rail tab. Replaces the boolean `seoOpen` that drove a right-hand
|
|
146
|
+
* aside — the same aside the page editor retired so the editor has one
|
|
147
|
+
* settings surface rather than two competing ones.
|
|
148
|
+
*/
|
|
149
|
+
const [railTab, setRailTab] = useState<RailTabId>('sections')
|
|
150
|
+
const seoOpen = railTab === 'seo'
|
|
133
151
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
134
152
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
135
153
|
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
136
154
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
137
155
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
138
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Take delivery of server state.
|
|
159
|
+
*
|
|
160
|
+
* Every path that receives a fresh post from the API goes through here so
|
|
161
|
+
* the reflow baseline is re-captured with it. Saving a PUBLISHED post writes
|
|
162
|
+
* straight through to the live content, so a save moves the baseline exactly
|
|
163
|
+
* as a publish does; missing one would leave the guard comparing against
|
|
164
|
+
* content no longer on the site.
|
|
165
|
+
*/
|
|
166
|
+
const acceptPost = useCallback((p: EditorPost) => {
|
|
167
|
+
setPost(p)
|
|
168
|
+
setBaseline(snapshotBaseline(p.sections, p.status))
|
|
169
|
+
}, [])
|
|
170
|
+
|
|
139
171
|
const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
|
|
140
172
|
const customRenderers = useAdminSectionRenderers()
|
|
141
173
|
const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
|
|
@@ -166,6 +198,45 @@ export function PostSectionEditor({
|
|
|
166
198
|
)
|
|
167
199
|
const publishReady = Boolean(post) && publishCheck.errors.length === 0
|
|
168
200
|
|
|
201
|
+
const railTabs = useMemo<RailTab[]>(
|
|
202
|
+
() => [
|
|
203
|
+
{
|
|
204
|
+
id: 'sections',
|
|
205
|
+
label: 'Sections',
|
|
206
|
+
title: 'Post content',
|
|
207
|
+
count: post?.sections.length ?? 0,
|
|
208
|
+
},
|
|
209
|
+
// No score dot yet. The page editor's comes from the page audit, which
|
|
210
|
+
// is page-scoped; wiring a number here from `computeLiveSeoScore` would
|
|
211
|
+
// put a different model's score on a tab, which is the disagreement the
|
|
212
|
+
// page editor was rewired to remove.
|
|
213
|
+
{ id: 'seo', label: 'SEO & GEO', title: 'Search & AI readiness' },
|
|
214
|
+
],
|
|
215
|
+
[post?.sections.length],
|
|
216
|
+
)
|
|
217
|
+
const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0]!
|
|
218
|
+
|
|
219
|
+
// Content the SEO pane analyses — sections plus the post body.
|
|
220
|
+
const seoContentHtml = useMemo(
|
|
221
|
+
() => buildEditorContentHtml(post?.sections ?? [], post?.body ?? ''),
|
|
222
|
+
[post?.sections, post?.body],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
// The reflow guard's inputs, gathered in one place. Memoised because a new
|
|
226
|
+
// object identity on every render would restart the debounce in every guarded
|
|
227
|
+
// field's effect — the measurement would then never settle while typing,
|
|
228
|
+
// which is precisely when it is wanted.
|
|
229
|
+
const reflowContext = useMemo(
|
|
230
|
+
() => ({
|
|
231
|
+
canvas: canvasEl,
|
|
232
|
+
sectionId: selectedId,
|
|
233
|
+
baseline,
|
|
234
|
+
device: viewport,
|
|
235
|
+
onProof: setViewport,
|
|
236
|
+
}),
|
|
237
|
+
[canvasEl, selectedId, baseline, viewport],
|
|
238
|
+
)
|
|
239
|
+
|
|
169
240
|
const liveSeoScore = useMemo(() => {
|
|
170
241
|
if (!post) return 0
|
|
171
242
|
return computeLiveSeoScore({
|
|
@@ -210,7 +281,7 @@ export function PostSectionEditor({
|
|
|
210
281
|
load
|
|
211
282
|
.then((p) => {
|
|
212
283
|
if (cancelled) return
|
|
213
|
-
|
|
284
|
+
acceptPost(p)
|
|
214
285
|
setSelectedId(null)
|
|
215
286
|
if (!documentId) {
|
|
216
287
|
setDirty(true)
|
|
@@ -305,7 +376,7 @@ export function PostSectionEditor({
|
|
|
305
376
|
setSaveState('saving')
|
|
306
377
|
try {
|
|
307
378
|
const saved = await savePostDraft(post)
|
|
308
|
-
|
|
379
|
+
acceptPost(saved)
|
|
309
380
|
setDirty(false)
|
|
310
381
|
setSaveState('saved')
|
|
311
382
|
setValidation(null)
|
|
@@ -387,7 +458,7 @@ export function PostSectionEditor({
|
|
|
387
458
|
setPublishing(true)
|
|
388
459
|
try {
|
|
389
460
|
const published = await publishPost(post)
|
|
390
|
-
|
|
461
|
+
acceptPost(published)
|
|
391
462
|
setDirty(false)
|
|
392
463
|
setSaveState('saved')
|
|
393
464
|
setValidation(null)
|
|
@@ -406,9 +477,9 @@ export function PostSectionEditor({
|
|
|
406
477
|
if (!post?.id) return
|
|
407
478
|
fetchPostForEditor(postType, post.id)
|
|
408
479
|
.then((p) => {
|
|
409
|
-
|
|
480
|
+
acceptPost(p)
|
|
410
481
|
setSelectedId(null)
|
|
411
|
-
|
|
482
|
+
setRailTab('sections')
|
|
412
483
|
setDirty(false)
|
|
413
484
|
setSaveState('idle')
|
|
414
485
|
setValidation(null)
|
|
@@ -421,23 +492,27 @@ export function PostSectionEditor({
|
|
|
421
492
|
const handleSeoSaved = useCallback(() => {
|
|
422
493
|
if (!post?.id) return
|
|
423
494
|
fetchPostForEditor(postType, post.id)
|
|
424
|
-
.then((p) =>
|
|
495
|
+
.then((p) => acceptPost(p))
|
|
425
496
|
.catch(() => {
|
|
426
497
|
/* SEO API saved; editor meta will sync on next full load */
|
|
427
498
|
})
|
|
428
499
|
}, [postType, post?.id])
|
|
429
500
|
|
|
430
501
|
const handleToggleSeo = useCallback(() => {
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
502
|
+
setRailTab((tab) => {
|
|
503
|
+
if (tab === 'seo') return 'sections'
|
|
504
|
+
setSelectedId(null)
|
|
505
|
+
return 'seo'
|
|
434
506
|
})
|
|
435
507
|
}, [])
|
|
436
508
|
|
|
437
509
|
const handleSelectSection = useCallback((id: string | null, fieldPath?: string) => {
|
|
438
|
-
|
|
439
|
-
setSelectedId(id)
|
|
510
|
+
setRailTab('sections')
|
|
440
511
|
setPendingFocusPath(fieldPath ?? null)
|
|
512
|
+
// Toggles, because selection is now disclosure — see the note on the page
|
|
513
|
+
// editor's copy. A `fieldPath` means the site preview is pointing at a
|
|
514
|
+
// specific field, which must open rather than toggle.
|
|
515
|
+
setSelectedId((current) => (fieldPath ? id : current === id ? null : id))
|
|
441
516
|
}, [])
|
|
442
517
|
|
|
443
518
|
const handleCanvasEdit = useCallback(
|
|
@@ -618,6 +693,11 @@ export function PostSectionEditor({
|
|
|
618
693
|
viewport={viewport}
|
|
619
694
|
backLabel="Posts"
|
|
620
695
|
onViewport={setViewport}
|
|
696
|
+
{...(sitePreview.active
|
|
697
|
+
? // The site preview is an iframe of the real front end, not a
|
|
698
|
+
// scaled canvas — a zoom percentage there would be a lie.
|
|
699
|
+
{}
|
|
700
|
+
: { zoom, canvasScale, onZoom: setZoom })}
|
|
621
701
|
onBack={() => guardedNavigate('/posts')}
|
|
622
702
|
onPreview={handlePreview}
|
|
623
703
|
onViewOnSite={post.id ? handleViewOnSite : undefined}
|
|
@@ -662,74 +742,96 @@ export function PostSectionEditor({
|
|
|
662
742
|
/>
|
|
663
743
|
)}
|
|
664
744
|
|
|
665
|
-
<
|
|
666
|
-
<
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
745
|
+
<ReflowContext.Provider value={reflowContext}>
|
|
746
|
+
<div className="prenta-editor-body relative flex flex-1 overflow-hidden">
|
|
747
|
+
<EditorRail collapsedLabel={activeRailTab.label}>
|
|
748
|
+
{({ width, dragging, collapse }) => (
|
|
749
|
+
<>
|
|
750
|
+
<EditorRailTabs tabs={railTabs} active={railTab} onChange={setRailTab} />
|
|
751
|
+
<RailHeader
|
|
752
|
+
title={activeRailTab.title}
|
|
753
|
+
width={width}
|
|
754
|
+
dragging={dragging}
|
|
755
|
+
onCollapse={collapse}
|
|
756
|
+
/>
|
|
757
|
+
|
|
758
|
+
<div
|
|
759
|
+
role="tabpanel"
|
|
760
|
+
id={`rail-panel-${railTab}`}
|
|
761
|
+
aria-labelledby={`rail-tab-${railTab}`}
|
|
762
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
763
|
+
>
|
|
764
|
+
{railTab === 'sections' ? (
|
|
765
|
+
<SectionsList
|
|
766
|
+
sections={post.sections}
|
|
767
|
+
selectedId={selectedId}
|
|
768
|
+
canEdit={canEdit}
|
|
769
|
+
onSelect={handleSelectSection}
|
|
770
|
+
onToggleVisibility={handleToggleVisibility}
|
|
771
|
+
renderEditor={(section) => (
|
|
772
|
+
<SectionEditForm
|
|
773
|
+
section={section}
|
|
774
|
+
canEdit={canEdit}
|
|
775
|
+
onContentChange={handleContentChange}
|
|
776
|
+
onMetaChange={handleMetaChange}
|
|
777
|
+
postBody={post.body}
|
|
778
|
+
onPostBodyChange={handlePostBodyChange}
|
|
779
|
+
/>
|
|
780
|
+
)}
|
|
781
|
+
/>
|
|
782
|
+
) : (
|
|
783
|
+
<SeoEditorPane
|
|
784
|
+
entityType="post"
|
|
785
|
+
entityId={post.id}
|
|
786
|
+
slug={post.slug}
|
|
787
|
+
auditSections={post.sections}
|
|
788
|
+
contentHtml={seoContentHtml}
|
|
789
|
+
draftMeta={{
|
|
790
|
+
seoTitle: post.seoTitle,
|
|
791
|
+
seoDescription: post.seoDescription,
|
|
792
|
+
}}
|
|
793
|
+
onDraftChange={handleFields}
|
|
794
|
+
onClose={() => setRailTab('sections')}
|
|
795
|
+
onSaved={handleSeoSaved}
|
|
796
|
+
/>
|
|
797
|
+
)}
|
|
798
|
+
</div>
|
|
799
|
+
</>
|
|
800
|
+
)}
|
|
801
|
+
</EditorRail>
|
|
802
|
+
|
|
803
|
+
<div className="flex-1 overflow-hidden">
|
|
804
|
+
{sitePreview.active ? (
|
|
805
|
+
<SitePreviewFrame
|
|
806
|
+
src={sitePreview.url}
|
|
807
|
+
viewport={viewport}
|
|
808
|
+
reloadKey={sitePreview.reloadKey}
|
|
809
|
+
title={`Site preview: ${post.title || `New ${typeLabel}`}`}
|
|
810
|
+
selectedId={selectedId}
|
|
811
|
+
sections={post.sections}
|
|
812
|
+
suppressSectionsEcho={suppressSectionsEcho}
|
|
813
|
+
onSelectSection={handleSelectSection}
|
|
814
|
+
onCanvasEdit={handleCanvasEdit}
|
|
815
|
+
onCanvasRequestMedia={handleCanvasRequestMedia}
|
|
816
|
+
/>
|
|
817
|
+
) : (
|
|
818
|
+
<PostEditorCanvas
|
|
819
|
+
sections={post.sections}
|
|
820
|
+
header={header}
|
|
821
|
+
context={context}
|
|
822
|
+
selectedId={selectedId}
|
|
823
|
+
viewport={viewport}
|
|
824
|
+
zoom={zoom}
|
|
825
|
+
onScaleChange={setCanvasScale}
|
|
826
|
+
onCanvasElement={setCanvasEl}
|
|
827
|
+
onSelect={handleSelectSection}
|
|
828
|
+
structuralTypes={structuralTypes}
|
|
829
|
+
sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
|
|
830
|
+
/>
|
|
831
|
+
)}
|
|
832
|
+
</div>
|
|
714
833
|
</div>
|
|
715
|
-
|
|
716
|
-
{seoOpen && (
|
|
717
|
-
<EditorSeoAside
|
|
718
|
-
entityType="post"
|
|
719
|
-
entityId={post.id}
|
|
720
|
-
title={post.title}
|
|
721
|
-
slug={post.slug}
|
|
722
|
-
seoTitle={post.seoTitle}
|
|
723
|
-
seoDescription={post.seoDescription}
|
|
724
|
-
sections={post.sections}
|
|
725
|
-
bodyHtml={post.body}
|
|
726
|
-
canEdit={canEdit}
|
|
727
|
-
onClose={() => setSeoOpen(false)}
|
|
728
|
-
onDraftChange={handleFields}
|
|
729
|
-
onSaved={handleSeoSaved}
|
|
730
|
-
/>
|
|
731
|
-
)}
|
|
732
|
-
</div>
|
|
834
|
+
</ReflowContext.Provider>
|
|
733
835
|
</div>
|
|
734
836
|
|
|
735
837
|
{post.id && (
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { PageSection } from '@prenta/core/page-sections';
|
|
2
|
-
export interface EditorSeoAsideProps {
|
|
3
|
-
entityType: 'page' | 'post';
|
|
4
|
-
/** Saved document id — when absent, draft mode with live analysis. */
|
|
5
|
-
entityId: string | null;
|
|
6
|
-
title: string;
|
|
7
|
-
slug: string;
|
|
8
|
-
seoTitle: string;
|
|
9
|
-
seoDescription: string;
|
|
10
|
-
/** Post body HTML and/or section content for live SEO analysis. */
|
|
11
|
-
sections?: PageSection[];
|
|
12
|
-
bodyHtml?: string;
|
|
13
|
-
canEdit: boolean;
|
|
14
|
-
onClose: () => void;
|
|
15
|
-
onDraftChange?: (patch: {
|
|
16
|
-
seoTitle?: string;
|
|
17
|
-
seoDescription?: string;
|
|
18
|
-
}) => void;
|
|
19
|
-
onSaved?: () => void;
|
|
20
|
-
}
|
|
21
|
-
/** Right-side SEO panel for the Page / Post section editors. */
|
|
22
|
-
export declare function EditorSeoAside({ entityType, entityId, slug, seoTitle, seoDescription, sections, bodyHtml, onClose, onDraftChange, onSaved, }: EditorSeoAsideProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
-
//# sourceMappingURL=EditorSeoAside.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EditorSeoAside.d.ts","sourceRoot":"","sources":["../../src/components/EditorSeoAside.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAI7D,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAC/E,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,cAAc,EACd,QAAa,EACb,QAAa,EACb,OAAO,EACP,aAAa,EACb,OAAO,GACR,EAAE,mBAAmB,2CA4BrB"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo } from 'react';
|
|
4
|
-
import { SeoEditorPane } from './seo/SeoEditorPane.js';
|
|
5
|
-
import { ResizableAside } from './ui/ResizableAside.js';
|
|
6
|
-
import { buildEditorContentHtml } from '../lib/editor-seo-content.js';
|
|
7
|
-
const INSPECTOR_SHELL = 'bg-card border-border flex h-full flex-col border-l';
|
|
8
|
-
/** Right-side SEO panel for the Page / Post section editors. */
|
|
9
|
-
export function EditorSeoAside({ entityType, entityId, slug, seoTitle, seoDescription, sections = [], bodyHtml = '', onClose, onDraftChange, onSaved, }) {
|
|
10
|
-
const contentHtml = useMemo(() => buildEditorContentHtml(sections, bodyHtml), [sections, bodyHtml]);
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
function onKey(e) {
|
|
13
|
-
if (e.key === 'Escape')
|
|
14
|
-
onClose();
|
|
15
|
-
}
|
|
16
|
-
document.addEventListener('keydown', onKey);
|
|
17
|
-
return () => document.removeEventListener('keydown', onKey);
|
|
18
|
-
}, [onClose]);
|
|
19
|
-
return (_jsx(ResizableAside, { role: "dialog", "aria-label": "SEO settings", className: INSPECTOR_SHELL, children: _jsx(SeoEditorPane, { entityType: entityType, entityId: entityId, slug: slug, contentHtml: contentHtml, draftMeta: { seoTitle, seoDescription }, onDraftChange: onDraftChange, onClose: onClose, onSaved: onSaved }) }));
|
|
20
|
-
}
|
|
21
|
-
//# sourceMappingURL=EditorSeoAside.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EditorSeoAside.js","sourceRoot":"","sources":["../../src/components/EditorSeoAside.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AAGrE,MAAM,eAAe,GAAG,qDAAqD,CAAA;AAmB7E,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,EAC7B,UAAU,EACV,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,cAAc,EACd,QAAQ,GAAG,EAAE,EACb,QAAQ,GAAG,EAAE,EACb,OAAO,EACP,aAAa,EACb,OAAO,GACa;IACpB,MAAM,WAAW,GAAG,OAAO,CACzB,GAAG,EAAE,CAAC,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAChD,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACrB,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,KAAK,CAAC,CAAgB;YAC7B,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;gBAAE,OAAO,EAAE,CAAA;QACnC,CAAC;QACD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAC3C,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IAC7D,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAEb,OAAO,CACL,KAAC,cAAc,IAAC,IAAI,EAAC,QAAQ,gBAAY,cAAc,EAAC,SAAS,EAAE,eAAe,YAChF,KAAC,aAAa,IACZ,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,EACvC,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,GAChB,GACa,CAClB,CAAA;AACH,CAAC"}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useMemo } from 'react'
|
|
4
|
-
import { SeoEditorPane } from './seo/SeoEditorPane.js'
|
|
5
|
-
import { ResizableAside } from './ui/ResizableAside.js'
|
|
6
|
-
import { buildEditorContentHtml } from '../lib/editor-seo-content.js'
|
|
7
|
-
import type { PageSection } from '@prenta/core/page-sections'
|
|
8
|
-
|
|
9
|
-
const INSPECTOR_SHELL = 'bg-card border-border flex h-full flex-col border-l'
|
|
10
|
-
|
|
11
|
-
export interface EditorSeoAsideProps {
|
|
12
|
-
entityType: 'page' | 'post'
|
|
13
|
-
/** Saved document id — when absent, draft mode with live analysis. */
|
|
14
|
-
entityId: string | null
|
|
15
|
-
title: string
|
|
16
|
-
slug: string
|
|
17
|
-
seoTitle: string
|
|
18
|
-
seoDescription: string
|
|
19
|
-
/** Post body HTML and/or section content for live SEO analysis. */
|
|
20
|
-
sections?: PageSection[]
|
|
21
|
-
bodyHtml?: string
|
|
22
|
-
canEdit: boolean
|
|
23
|
-
onClose: () => void
|
|
24
|
-
onDraftChange?: (patch: { seoTitle?: string; seoDescription?: string }) => void
|
|
25
|
-
onSaved?: () => void
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Right-side SEO panel for the Page / Post section editors. */
|
|
29
|
-
export function EditorSeoAside({
|
|
30
|
-
entityType,
|
|
31
|
-
entityId,
|
|
32
|
-
slug,
|
|
33
|
-
seoTitle,
|
|
34
|
-
seoDescription,
|
|
35
|
-
sections = [],
|
|
36
|
-
bodyHtml = '',
|
|
37
|
-
onClose,
|
|
38
|
-
onDraftChange,
|
|
39
|
-
onSaved,
|
|
40
|
-
}: EditorSeoAsideProps) {
|
|
41
|
-
const contentHtml = useMemo(
|
|
42
|
-
() => buildEditorContentHtml(sections, bodyHtml),
|
|
43
|
-
[sections, bodyHtml],
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
function onKey(e: KeyboardEvent) {
|
|
48
|
-
if (e.key === 'Escape') onClose()
|
|
49
|
-
}
|
|
50
|
-
document.addEventListener('keydown', onKey)
|
|
51
|
-
return () => document.removeEventListener('keydown', onKey)
|
|
52
|
-
}, [onClose])
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<ResizableAside role="dialog" aria-label="SEO settings" className={INSPECTOR_SHELL}>
|
|
56
|
-
<SeoEditorPane
|
|
57
|
-
entityType={entityType}
|
|
58
|
-
entityId={entityId}
|
|
59
|
-
slug={slug}
|
|
60
|
-
contentHtml={contentHtml}
|
|
61
|
-
draftMeta={{ seoTitle, seoDescription }}
|
|
62
|
-
onDraftChange={onDraftChange}
|
|
63
|
-
onClose={onClose}
|
|
64
|
-
onSaved={onSaved}
|
|
65
|
-
/>
|
|
66
|
-
</ResizableAside>
|
|
67
|
-
)
|
|
68
|
-
}
|