@prenta/admin 0.95.1 → 0.97.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 +76 -0
- package/dist/__tests__/lib/page-editor-service.test.js +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
- package/dist/__tests__/lib/post-editor-service.test.js +27 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- 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/post-section-editor.test.js +42 -0
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/version-history-panel.render.test.js +23 -2
- package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -1
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +7 -0
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +11 -1
- package/dist/views/Posts/PostsListView.js.map +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/SectionEditForm.d.ts.map +1 -1
- package/dist/views/page-editor/SectionEditForm.js +6 -1
- package/dist/views/page-editor/SectionEditForm.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +5 -1
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/sections/DividerSection.d.ts +14 -0
- package/dist/views/page-editor/sections/DividerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/DividerSection.js +19 -0
- package/dist/views/page-editor/sections/DividerSection.js.map +1 -0
- package/dist/views/page-editor/sections/EmbedSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/EmbedSection.js +11 -3
- package/dist/views/page-editor/sections/EmbedSection.js.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +4 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/page-editor/sections/WidgetEmbedSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/WidgetEmbedSection.js +1 -1
- package/dist/views/page-editor/sections/WidgetEmbedSection.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 +82 -22
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/lib/page-editor-service.test.ts +1 -0
- package/src/__tests__/lib/post-editor-service.test.ts +30 -0
- 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/post-section-editor.test.tsx +63 -0
- package/src/__tests__/views/version-history-panel.render.test.tsx +26 -2
- package/src/lib/post-editor-service.ts +7 -0
- package/src/views/Posts/PostsListView.tsx +17 -1
- package/src/views/page-editor/CanvasSurface.tsx +171 -0
- package/src/views/page-editor/EditorCanvas.tsx +46 -144
- package/src/views/page-editor/SectionEditForm.tsx +9 -1
- package/src/views/page-editor/SectionInspector.tsx +7 -2
- package/src/views/page-editor/sections/DividerSection.tsx +31 -0
- package/src/views/page-editor/sections/EmbedSection.tsx +11 -5
- package/src/views/page-editor/sections/SectionRenderer.tsx +4 -0
- package/src/views/page-editor/sections/WidgetEmbedSection.tsx +4 -1
- package/src/views/post-editor/PostEditorCanvas.tsx +54 -74
- package/src/views/post-editor/PostSectionEditor.tsx +180 -81
- 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,10 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
4
3
|
import { LayoutTemplate } from 'lucide-react'
|
|
5
4
|
import { CanvasEditTip } from '../../components/CanvasEditTip.js'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { CanvasScroll, CanvasSurface, useSectionRefs } from './CanvasSurface.js'
|
|
6
|
+
import type { BreakpointId, Zoom } from '../../lib/breakpoints.js'
|
|
8
7
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
9
8
|
import { SectionRenderer } from './sections/index.js'
|
|
10
9
|
import { StructureNotice } from './StructureNotice.js'
|
|
@@ -15,17 +14,7 @@ interface EditorCanvasProps {
|
|
|
15
14
|
breakpoint: BreakpointId
|
|
16
15
|
zoom?: Zoom
|
|
17
16
|
onSelect: (id: string) => void
|
|
18
|
-
/**
|
|
19
|
-
* Reports the measured scale so the header's zoom chip can show a real
|
|
20
|
-
* percentage. Only the canvas knows the available width, so the number has
|
|
21
|
-
* to travel up rather than being recomputed by the header.
|
|
22
|
-
*/
|
|
23
17
|
onScaleChange?: (scale: number) => void
|
|
24
|
-
/**
|
|
25
|
-
* Reports the canvas surface element, so the reflow guard can clone it. The
|
|
26
|
-
* guard needs the real element rather than a `document.querySelector`, which
|
|
27
|
-
* would also match the site-preview frame's canvas when that is mounted.
|
|
28
|
-
*/
|
|
29
18
|
onCanvasElement?: (el: HTMLElement | null) => void
|
|
30
19
|
/** Section types rendered as placeholders — shows the "Structure view" notice. */
|
|
31
20
|
structuralTypes?: string[]
|
|
@@ -46,24 +35,11 @@ interface EditorCanvasProps {
|
|
|
46
35
|
}
|
|
47
36
|
|
|
48
37
|
/**
|
|
49
|
-
* The preview canvas.
|
|
50
|
-
*
|
|
51
|
-
* The canvas renders at the breakpoint's **real CSS width** and is scaled down
|
|
52
|
-
* with a transform. Combined with `container-type: inline-size`, that makes the
|
|
53
|
-
* page reflow against its own width — the same container queries that style the
|
|
54
|
-
* live page decide the layout. A max-width clamp cannot do this: it yields a
|
|
55
|
-
* narrow desktop layout rather than the mobile one.
|
|
38
|
+
* The page editor's preview canvas.
|
|
56
39
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* panel's first child, not from `panel.clientWidth - padding`. The two
|
|
61
|
-
* disagree once a scrollbar appears or the host scales the preview, and
|
|
62
|
-
* the padding arithmetic silently rots whenever the gutter changes.
|
|
63
|
-
* 2. Both measurements are re-taken on **every render**, not only from the
|
|
64
|
-
* ResizeObserver. Page height changes with every text edit, image swap and
|
|
65
|
-
* show/hide — none of which resize the observed elements. The observer
|
|
66
|
-
* exists for later window resizes; its first callback is not a substitute.
|
|
40
|
+
* Measurement, scaling and the fit box live in {@link CanvasSurface}, shared
|
|
41
|
+
* with the post editor. What is left here is what is genuinely page-specific:
|
|
42
|
+
* which chrome sits above the canvas, and what goes inside it.
|
|
67
43
|
*/
|
|
68
44
|
export function EditorCanvas({
|
|
69
45
|
sections,
|
|
@@ -79,70 +55,31 @@ export function EditorCanvas({
|
|
|
79
55
|
changedSectionIds,
|
|
80
56
|
reviewBar,
|
|
81
57
|
}: EditorCanvasProps) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// No dependency array: page height changes on edits that resize nothing the
|
|
96
|
-
// observer watches. Both setters bail when the value is unchanged, so this
|
|
97
|
-
// settles after one extra pass instead of looping.
|
|
98
|
-
useLayoutEffect(() => {
|
|
99
|
-
const measure = () => {
|
|
100
|
-
const sentinelWidth = sentinelRef.current?.offsetWidth ?? 0
|
|
101
|
-
if (sentinelWidth > 0) setAvailable((prev) => (prev === sentinelWidth ? prev : sentinelWidth))
|
|
102
|
-
|
|
103
|
-
const height = surfaceRef.current?.offsetHeight ?? 0
|
|
104
|
-
if (height > 0) setPageHeight((prev) => (prev === height ? prev : height))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
measure()
|
|
108
|
-
|
|
109
|
-
const ro = new ResizeObserver(measure)
|
|
110
|
-
if (sentinelRef.current) ro.observe(sentinelRef.current)
|
|
111
|
-
if (surfaceRef.current) ro.observe(surfaceRef.current)
|
|
112
|
-
return () => ro.disconnect()
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
// Scroll the selected section into view within the canvas.
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
if (!selectedId) return
|
|
118
|
-
const el = sectionRefs.current.get(selectedId)
|
|
119
|
-
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
120
|
-
}, [selectedId])
|
|
121
|
-
|
|
122
|
-
const scale = fitScale(available, bp.width, zoom)
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
onScaleChange?.(scale)
|
|
126
|
-
}, [scale, onScaleChange])
|
|
58
|
+
const registerRef = useSectionRefs(selectedId)
|
|
59
|
+
|
|
60
|
+
// A design-owned page always ships the template's sections, so an empty
|
|
61
|
+
// canvas means the template resolved to nothing — a configuration problem,
|
|
62
|
+
// not a page to scale and measure.
|
|
63
|
+
if (sections.length === 0) {
|
|
64
|
+
return (
|
|
65
|
+
<CanvasScroll>
|
|
66
|
+
<EmptyCanvas />
|
|
67
|
+
</CanvasScroll>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
127
70
|
|
|
128
71
|
return (
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
) : (
|
|
141
|
-
<>
|
|
142
|
-
{/* The review bar REPLACES the editing chrome rather than joining it:
|
|
143
|
-
a tip about clicking sections to edit them is actively wrong while
|
|
144
|
-
the canvas is showing a version you cannot edit. */}
|
|
145
|
-
{reviewing ? (
|
|
72
|
+
<CanvasScroll>
|
|
73
|
+
<CanvasSurface
|
|
74
|
+
breakpoint={breakpoint}
|
|
75
|
+
zoom={zoom}
|
|
76
|
+
onScaleChange={onScaleChange}
|
|
77
|
+
onCanvasElement={onCanvasElement}
|
|
78
|
+
chrome={
|
|
79
|
+
// The review bar REPLACES the editing chrome rather than joining it:
|
|
80
|
+
// a tip about clicking sections to edit them is actively wrong while
|
|
81
|
+
// the canvas is showing a version you cannot edit.
|
|
82
|
+
reviewing ? (
|
|
146
83
|
reviewBar
|
|
147
84
|
) : (
|
|
148
85
|
<>
|
|
@@ -152,58 +89,23 @@ export function EditorCanvas({
|
|
|
152
89
|
sitePreviewAvailable={sitePreviewAvailable}
|
|
153
90
|
/>
|
|
154
91
|
</>
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{/* `prenta-canvas` is the public hook for consumer brand CSS:
|
|
173
|
-
site styles that should render in this preview scope to
|
|
174
|
-
`.prenta-admin .prenta-canvas` instead of the admin root. */}
|
|
175
|
-
<div
|
|
176
|
-
ref={(el) => {
|
|
177
|
-
surfaceRef.current = el
|
|
178
|
-
onCanvasElement?.(el)
|
|
179
|
-
}}
|
|
180
|
-
// `prenta-canvas` carries `container-type: inline-size` (theme.css)
|
|
181
|
-
// — that is what makes the `@pv-*` steps in the section renderers
|
|
182
|
-
// resolve against the simulated width instead of the viewport.
|
|
183
|
-
className="prenta-canvas border-border bg-card origin-top-left overflow-hidden rounded-lg text-left shadow-[0_2px_20px_rgba(0,0,0,0.12)] motion-safe:transition-[width,transform] motion-safe:duration-(--mo-slow) motion-safe:ease-(--ease)"
|
|
184
|
-
style={{
|
|
185
|
-
width: bp.width,
|
|
186
|
-
transform: `scale(${scale})`,
|
|
187
|
-
transformOrigin: 'top left',
|
|
188
|
-
}}
|
|
189
|
-
>
|
|
190
|
-
{sections.map((section) => (
|
|
191
|
-
<SectionRenderer
|
|
192
|
-
key={section.id}
|
|
193
|
-
section={section}
|
|
194
|
-
editable
|
|
195
|
-
reviewing={reviewing}
|
|
196
|
-
changed={changedSectionIds?.has(section.id) ?? false}
|
|
197
|
-
selected={section.id === selectedId}
|
|
198
|
-
onSelect={onSelect}
|
|
199
|
-
registerRef={registerRef}
|
|
200
|
-
/>
|
|
201
|
-
))}
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
</>
|
|
205
|
-
)}
|
|
206
|
-
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
{sections.map((section) => (
|
|
96
|
+
<SectionRenderer
|
|
97
|
+
key={section.id}
|
|
98
|
+
section={section}
|
|
99
|
+
editable
|
|
100
|
+
reviewing={reviewing}
|
|
101
|
+
changed={changedSectionIds?.has(section.id) ?? false}
|
|
102
|
+
selected={section.id === selectedId}
|
|
103
|
+
onSelect={onSelect}
|
|
104
|
+
registerRef={registerRef}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</CanvasSurface>
|
|
108
|
+
</CanvasScroll>
|
|
207
109
|
)
|
|
208
110
|
}
|
|
209
111
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { ExternalLink, Lock } from 'lucide-react'
|
|
4
4
|
import { RichTextField } from '../../fields/RichTextField.js'
|
|
5
5
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
6
|
+
import { visibleFields } from '@prenta/core/page-sections'
|
|
6
7
|
import { ContentField, FIELD_INPUT, FIELD_LABEL } from './section-fields.js'
|
|
7
8
|
import { getSectionType } from './section-types.js'
|
|
8
9
|
|
|
@@ -75,7 +76,14 @@ export function SectionEditForm({
|
|
|
75
76
|
section.sectionType === 'article-body' &&
|
|
76
77
|
bodySource === 'field' &&
|
|
77
78
|
onPostBodyChange !== undefined
|
|
78
|
-
|
|
79
|
+
// `visibleFields` first, so a type offering alternative inputs for one job
|
|
80
|
+
// (see the video section's source switch) shows only the box that is live.
|
|
81
|
+
// The validator reads the same helper, so it can never require a field the
|
|
82
|
+
// editor is not drawing.
|
|
83
|
+
const contentFields =
|
|
84
|
+
visibleFields(def?.fields ?? [], section.content).filter(
|
|
85
|
+
(f) => !(editPostBodyField && f.key === 'body'),
|
|
86
|
+
) ?? []
|
|
79
87
|
|
|
80
88
|
return (
|
|
81
89
|
<div className="border-border bg-muted/40 border-t px-3 py-3.5">
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useEffect } from 'react'
|
|
4
4
|
import { Eye, EyeOff, Lock, X } from 'lucide-react'
|
|
5
5
|
import { InspectorExpandButton, ResizableAside } from '../../components/ui/ResizableAside.js'
|
|
6
|
+
import { visibleFields } from '@prenta/core/page-sections'
|
|
6
7
|
import { ContentField, FIELD_INPUT, FIELD_LABEL } from './section-fields.js'
|
|
7
8
|
import { RichTextField } from '../../fields/RichTextField.js'
|
|
8
9
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
@@ -66,8 +67,12 @@ export function SectionInspector({
|
|
|
66
67
|
section.sectionType === 'article-body' &&
|
|
67
68
|
bodySource === 'field' &&
|
|
68
69
|
onPostBodyChange !== undefined
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
// Same visibility rule as the inline form and the validator — see
|
|
71
|
+
// `visibleFields`. The template inspector must not offer a box the page
|
|
72
|
+
// editor hides, or the two surfaces disagree about the same section.
|
|
73
|
+
const contentFields = visibleFields(def?.fields ?? [], section.content).filter(
|
|
74
|
+
(field) => !(editPostBodyField && field.key === 'body'),
|
|
75
|
+
)
|
|
71
76
|
|
|
72
77
|
// Escape closes the inspector.
|
|
73
78
|
useEffect(() => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
2
|
+
import { str } from './parts.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canvas body for the `divider` section.
|
|
6
|
+
*
|
|
7
|
+
* `space` deliberately renders an empty box rather than nothing: on the public
|
|
8
|
+
* page the section wrapper's padding IS the break, but on the canvas a section
|
|
9
|
+
* that draws nothing at all reads as broken. A faint dashed outline says "this
|
|
10
|
+
* is intentional whitespace" without pretending to be what ships — the canvas
|
|
11
|
+
* shows structure, and empty space is the structure here.
|
|
12
|
+
*/
|
|
13
|
+
export function DividerSection({ section }: { section: PageSection }) {
|
|
14
|
+
const variant = str(section.content, 'variant') || 'line'
|
|
15
|
+
|
|
16
|
+
if (variant === 'space') {
|
|
17
|
+
return (
|
|
18
|
+
<div className="mx-auto max-w-3xl px-6">
|
|
19
|
+
<div className="border-border text-muted-foreground flex h-10 items-center justify-center rounded-md border border-dashed text-xs">
|
|
20
|
+
Space
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="mx-auto max-w-3xl px-6">
|
|
28
|
+
<hr className="border-0 border-t border-current opacity-15" aria-hidden />
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PlayCircle } from 'lucide-react'
|
|
2
|
-
import {
|
|
2
|
+
import { resolveVideo, EMBED_PROVIDER_LABEL } from '@prenta/core/page-sections'
|
|
3
3
|
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
4
4
|
import { str } from './parts.js'
|
|
5
5
|
|
|
@@ -17,10 +17,18 @@ const RATIO_CLASS: Record<string, string> = {
|
|
|
17
17
|
*/
|
|
18
18
|
export function EmbedSection({ section }: { section: PageSection }) {
|
|
19
19
|
const c = section.content
|
|
20
|
-
const
|
|
20
|
+
const video = resolveVideo(c)
|
|
21
21
|
const ratioClass = RATIO_CLASS[str(c, 'ratio', '16:9')] ?? RATIO_CLASS['16:9']
|
|
22
22
|
const title = str(c, 'title')
|
|
23
23
|
const caption = str(c, 'caption')
|
|
24
|
+
// Naming the resolved source is the only confirmation an editor gets that
|
|
25
|
+
// the link they pasted, or the file they picked, actually resolved — the
|
|
26
|
+
// canvas never loads the real video either way.
|
|
27
|
+
const label = !video
|
|
28
|
+
? 'No valid video set'
|
|
29
|
+
: video.kind === 'hosted'
|
|
30
|
+
? 'Uploaded video'
|
|
31
|
+
: `${EMBED_PROVIDER_LABEL[video.provider]} video`
|
|
24
32
|
|
|
25
33
|
return (
|
|
26
34
|
<figure className="mx-auto max-w-3xl px-6">
|
|
@@ -28,9 +36,7 @@ export function EmbedSection({ section }: { section: PageSection }) {
|
|
|
28
36
|
className={`bg-muted text-muted-foreground relative flex w-full flex-col items-center justify-center gap-2 overflow-hidden rounded-xl ${ratioClass}`}
|
|
29
37
|
>
|
|
30
38
|
<PlayCircle className="h-10 w-10" aria-hidden />
|
|
31
|
-
<p className="text-foreground text-sm font-medium">
|
|
32
|
-
{parsed ? `${EMBED_PROVIDER_LABEL[parsed.provider]} video` : 'No valid video URL'}
|
|
33
|
-
</p>
|
|
39
|
+
<p className="text-foreground text-sm font-medium">{label}</p>
|
|
34
40
|
{title && <p className="text-muted-foreground px-4 text-center text-xs">{title}</p>}
|
|
35
41
|
</div>
|
|
36
42
|
{caption && (
|
|
@@ -18,6 +18,7 @@ import { RelatedPostsSection } from './RelatedPostsSection.js'
|
|
|
18
18
|
import { GallerySection } from './GallerySection.js'
|
|
19
19
|
import { EmbedSection } from './EmbedSection.js'
|
|
20
20
|
import { WidgetEmbedSection } from './WidgetEmbedSection.js'
|
|
21
|
+
import { DividerSection } from './DividerSection.js'
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Section types with a real canvas body component below. Anything else —
|
|
@@ -38,6 +39,7 @@ const CANVAS_RENDERED_TYPES: ReadonlySet<string> = new Set([
|
|
|
38
39
|
'gallery',
|
|
39
40
|
'embed',
|
|
40
41
|
'widget-embed',
|
|
42
|
+
'divider',
|
|
41
43
|
])
|
|
42
44
|
|
|
43
45
|
/** True when the canvas renders this type for real (not as a placeholder). */
|
|
@@ -126,6 +128,8 @@ function BuiltInSectionBody({
|
|
|
126
128
|
return <EmbedSection section={section} />
|
|
127
129
|
case 'widget-embed':
|
|
128
130
|
return <WidgetEmbedSection section={section} />
|
|
131
|
+
case 'divider':
|
|
132
|
+
return <DividerSection section={section} />
|
|
129
133
|
default:
|
|
130
134
|
// Config-declared custom types and unmanaged sections with no supplied
|
|
131
135
|
// renderer: a neutral placeholder keeps the canvas reflecting the
|
|
@@ -20,10 +20,13 @@ export function WidgetEmbedSection({ section }: { section: PageSection }) {
|
|
|
20
20
|
style={{ minHeight: `${minHeight}px` }}
|
|
21
21
|
>
|
|
22
22
|
<Puzzle className="h-8 w-8" aria-hidden />
|
|
23
|
+
{/* Naming the recognised provider is the only feedback an editor gets
|
|
24
|
+
that their pasted snippet parsed — the widget itself never loads in
|
|
25
|
+
the canvas, so "it looks blank" is the expected state either way. */}
|
|
23
26
|
<p className="text-foreground text-sm font-medium">
|
|
24
27
|
{parsed
|
|
25
28
|
? `${WIDGET_PROVIDER_LABEL[parsed.provider]} widget`
|
|
26
|
-
: '
|
|
29
|
+
: 'Paste embed code to load a widget'}
|
|
27
30
|
</p>
|
|
28
31
|
{title && <p className="text-muted-foreground px-4 text-center text-xs">{title}</p>}
|
|
29
32
|
</div>
|
|
@@ -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
|
|