@nuasite/cms 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/editor.js +8416 -10418
- package/package.json +1 -1
- package/src/editor/components/toolbar.tsx +9 -9
- package/src/editor/hooks/useElementDetection.ts +4 -46
- package/src/editor/index.tsx +107 -42
- package/src/editor/post-message.ts +136 -0
- package/src/editor/signals.ts +1 -0
- package/src/index.ts +9 -0
- package/src/types.ts +145 -0
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ export interface ToolbarCallbacks {
|
|
|
10
10
|
onCompare: () => void
|
|
11
11
|
onSave: () => void
|
|
12
12
|
onDiscard: () => void
|
|
13
|
-
|
|
13
|
+
onSelectElement?: () => void
|
|
14
14
|
onMediaLibrary?: () => void
|
|
15
15
|
onDismissDeployment?: () => void
|
|
16
16
|
onNavigateChange?: () => void
|
|
@@ -97,7 +97,6 @@ const DeploymentStatusIndicator = ({ onDismiss }: { onDismiss?: () => void }) =>
|
|
|
97
97
|
export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
98
98
|
const isEditing = signals.isEditing.value
|
|
99
99
|
const showingOriginal = signals.showingOriginal.value
|
|
100
|
-
const isChatOpen = signals.isChatOpen.value
|
|
101
100
|
const dirtyCount = signals.totalDirtyCount.value
|
|
102
101
|
const isSaving = signals.isSaving.value
|
|
103
102
|
const deploymentStatus = signals.deploymentStatus.value
|
|
@@ -107,7 +106,6 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
107
106
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
|
108
107
|
|
|
109
108
|
if (isPreviewingMarkdown) return null
|
|
110
|
-
if (isChatOpen && !isEditing) return null
|
|
111
109
|
|
|
112
110
|
const showDeploymentStatus = deploymentStatus !== null
|
|
113
111
|
|
|
@@ -126,21 +124,23 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
|
|
|
126
124
|
}
|
|
127
125
|
}
|
|
128
126
|
|
|
129
|
-
const
|
|
127
|
+
const isSelectMode = signals.isSelectMode.value
|
|
128
|
+
const isToolbarOpen = isEditing || isSelectMode
|
|
130
129
|
|
|
131
130
|
// Build menu items dynamically
|
|
132
131
|
const menuItems: Array<{ label: string; icon: ComponentChildren; onClick: () => void; isActive?: boolean }> = []
|
|
133
132
|
|
|
134
|
-
if (callbacks.
|
|
133
|
+
if (callbacks.onSelectElement) {
|
|
135
134
|
menuItems.push({
|
|
136
|
-
label: '
|
|
135
|
+
label: 'Select Element',
|
|
137
136
|
icon: (
|
|
138
137
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
139
|
-
<path d="
|
|
138
|
+
<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" />
|
|
139
|
+
<path d="M13 13l6 6" />
|
|
140
140
|
</svg>
|
|
141
141
|
),
|
|
142
|
-
onClick: () => callbacks.
|
|
143
|
-
isActive:
|
|
142
|
+
onClick: () => callbacks.onSelectElement?.(),
|
|
143
|
+
isActive: isSelectMode,
|
|
144
144
|
})
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -58,9 +58,9 @@ export function useElementDetection(): OutlineState {
|
|
|
58
58
|
useEffect(() => {
|
|
59
59
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
60
60
|
const isEditing = signals.isEditing.value
|
|
61
|
-
const
|
|
61
|
+
const selectMode = signals.isSelectMode.value
|
|
62
62
|
|
|
63
|
-
if (!isEditing && !
|
|
63
|
+
if (!isEditing && !selectMode) {
|
|
64
64
|
if (hideTimeoutRef.current) {
|
|
65
65
|
clearTimeout(hideTimeoutRef.current)
|
|
66
66
|
hideTimeoutRef.current = null
|
|
@@ -89,34 +89,6 @@ export function useElementDetection(): OutlineState {
|
|
|
89
89
|
const manifest = signals.manifest.value
|
|
90
90
|
const entries = manifest.entries
|
|
91
91
|
|
|
92
|
-
// When chat is open, only detect components (not text/image elements)
|
|
93
|
-
if (chatOpen) {
|
|
94
|
-
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
95
|
-
if (componentEl) {
|
|
96
|
-
if (hideTimeoutRef.current) {
|
|
97
|
-
clearTimeout(hideTimeoutRef.current)
|
|
98
|
-
hideTimeoutRef.current = null
|
|
99
|
-
}
|
|
100
|
-
const rect = componentEl.getBoundingClientRect()
|
|
101
|
-
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
102
|
-
const instance = componentId ? getComponentInstance(manifest, componentId) : null
|
|
103
|
-
|
|
104
|
-
setOutlineState({
|
|
105
|
-
visible: true,
|
|
106
|
-
rect,
|
|
107
|
-
isComponent: true,
|
|
108
|
-
componentName: instance?.componentName,
|
|
109
|
-
tagName: componentEl.tagName.toLowerCase(),
|
|
110
|
-
element: componentEl,
|
|
111
|
-
cmsId: null,
|
|
112
|
-
})
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
setOutlineState(INITIAL_STATE)
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
|
|
120
92
|
// Use the improved elementsFromPoint-based detection
|
|
121
93
|
const cmsEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
|
|
122
94
|
|
|
@@ -222,8 +194,8 @@ export function useComponentClickHandler({
|
|
|
222
194
|
useEffect(() => {
|
|
223
195
|
const handleClick = (ev: MouseEvent) => {
|
|
224
196
|
const isEditing = signals.isEditing.value
|
|
225
|
-
const
|
|
226
|
-
if (!isEditing && !
|
|
197
|
+
const selectMode = signals.isSelectMode.value
|
|
198
|
+
if (!isEditing && !selectMode) return
|
|
227
199
|
|
|
228
200
|
// Ignore clicks on CMS UI elements
|
|
229
201
|
if (isEventOnCmsUI(ev)) return
|
|
@@ -231,20 +203,6 @@ export function useComponentClickHandler({
|
|
|
231
203
|
const manifest = signals.manifest.value
|
|
232
204
|
const entries = manifest.entries
|
|
233
205
|
|
|
234
|
-
if (chatOpen) {
|
|
235
|
-
// When chat is open, only select components
|
|
236
|
-
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
237
|
-
if (componentEl) {
|
|
238
|
-
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
239
|
-
if (componentId) {
|
|
240
|
-
ev.preventDefault()
|
|
241
|
-
ev.stopPropagation()
|
|
242
|
-
signals.setChatContextElement(componentId)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
|
-
|
|
248
206
|
// Normal editing mode behavior
|
|
249
207
|
// Check for text element first
|
|
250
208
|
const textEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
|
package/src/editor/index.tsx
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { render } from 'preact'
|
|
2
|
-
import { useCallback, useEffect } from 'preact/hooks'
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
|
3
|
+
import type { CmsElementDeselectedMessage, CmsElementSelectedMessage } from '../types'
|
|
3
4
|
import { fetchManifest } from './api'
|
|
4
|
-
import { AIChat } from './components/ai-chat'
|
|
5
|
-
import { AITooltip } from './components/ai-tooltip'
|
|
6
5
|
import { AttributeEditor } from './components/attribute-editor'
|
|
7
6
|
import { BgImageOverlay } from './components/bg-image-overlay'
|
|
8
7
|
import { BlockEditor } from './components/block-editor'
|
|
@@ -32,9 +31,8 @@ import {
|
|
|
32
31
|
stopEditMode,
|
|
33
32
|
toggleShowOriginal,
|
|
34
33
|
} from './editor'
|
|
35
|
-
import { performRedo, performUndo } from './history'
|
|
34
|
+
import { canRedo, canUndo, performRedo, performUndo } from './history'
|
|
36
35
|
import {
|
|
37
|
-
useAIHandlers,
|
|
38
36
|
useBgImageHoverDetection,
|
|
39
37
|
useBlockEditorHandlers,
|
|
40
38
|
useComponentClickHandler,
|
|
@@ -43,6 +41,14 @@ import {
|
|
|
43
41
|
useTextSelection,
|
|
44
42
|
useTooltipState,
|
|
45
43
|
} from './hooks'
|
|
44
|
+
import {
|
|
45
|
+
buildEditorState,
|
|
46
|
+
buildPageNavigatedMessage,
|
|
47
|
+
buildReadyMessage,
|
|
48
|
+
buildSelectedElement,
|
|
49
|
+
buildStateChangedMessage,
|
|
50
|
+
postToParent,
|
|
51
|
+
} from './post-message'
|
|
46
52
|
import {
|
|
47
53
|
openCollectionsBrowser,
|
|
48
54
|
openMarkdownEditorForCurrentPage,
|
|
@@ -103,6 +109,17 @@ const CmsUI = () => {
|
|
|
103
109
|
}).catch(() => {})
|
|
104
110
|
}, [])
|
|
105
111
|
|
|
112
|
+
// Re-fetch manifest on View Transitions navigation (astro:after-swap)
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const onNavigation = () => {
|
|
115
|
+
fetchManifest().then((manifest) => {
|
|
116
|
+
signals.setManifest(manifest)
|
|
117
|
+
}).catch(() => {})
|
|
118
|
+
}
|
|
119
|
+
document.addEventListener('astro:after-swap', onNavigation)
|
|
120
|
+
return () => document.removeEventListener('astro:after-swap', onNavigation)
|
|
121
|
+
}, [])
|
|
122
|
+
|
|
106
123
|
// Auto-restore edit mode if it was active before a page refresh (e.g. after save triggers HMR)
|
|
107
124
|
useEffect(() => {
|
|
108
125
|
if (loadEditingState() && !signals.isEditing.value) {
|
|
@@ -117,18 +134,85 @@ const CmsUI = () => {
|
|
|
117
134
|
}
|
|
118
135
|
}, [])
|
|
119
136
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
// Send selected element info to parent window via postMessage (when inside an iframe)
|
|
138
|
+
const prevOutlineRef = useRef<{ cmsId: string | null; isComponent: boolean }>({ cmsId: null, isComponent: false })
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const prev = prevOutlineRef.current
|
|
141
|
+
const changed = outlineState.cmsId !== prev.cmsId
|
|
142
|
+
|| outlineState.isComponent !== prev.isComponent
|
|
143
|
+
|| (!outlineState.visible && (prev.cmsId !== null || prev.isComponent))
|
|
144
|
+
|
|
145
|
+
if (!changed) return
|
|
146
|
+
prevOutlineRef.current = { cmsId: outlineState.cmsId, isComponent: outlineState.isComponent }
|
|
147
|
+
|
|
148
|
+
if (outlineState.visible && (outlineState.cmsId || outlineState.isComponent)) {
|
|
149
|
+
const manifestData = signals.manifest.value
|
|
150
|
+
const entry = outlineState.cmsId ? manifestData.entries[outlineState.cmsId] : undefined
|
|
151
|
+
const componentEl = outlineState.element
|
|
152
|
+
const componentId = componentEl?.getAttribute('data-cms-component-id') ?? undefined
|
|
153
|
+
const instance = componentId ? manifestData.components?.[componentId] : undefined
|
|
154
|
+
const rect = outlineState.rect
|
|
155
|
+
|
|
156
|
+
const msg: CmsElementSelectedMessage = {
|
|
157
|
+
type: 'cms-element-selected',
|
|
158
|
+
element: buildSelectedElement({
|
|
159
|
+
cmsId: outlineState.cmsId,
|
|
160
|
+
isComponent: outlineState.isComponent,
|
|
161
|
+
componentName: outlineState.componentName,
|
|
162
|
+
componentId,
|
|
163
|
+
tagName: outlineState.tagName,
|
|
164
|
+
rect: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null,
|
|
165
|
+
entry,
|
|
166
|
+
instance,
|
|
167
|
+
}),
|
|
168
|
+
}
|
|
169
|
+
postToParent(msg)
|
|
170
|
+
} else {
|
|
171
|
+
const msg: CmsElementDeselectedMessage = { type: 'cms-element-deselected' }
|
|
172
|
+
postToParent(msg)
|
|
173
|
+
}
|
|
174
|
+
}, [outlineState])
|
|
175
|
+
|
|
176
|
+
// Send cms-ready + cms-page-navigated when manifest loads
|
|
177
|
+
const prevManifestRef = useRef<boolean>(false)
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const m = signals.manifest.value
|
|
180
|
+
// Only fire when manifest has entries (i.e. actually loaded)
|
|
181
|
+
if (Object.keys(m.entries).length === 0) return
|
|
182
|
+
|
|
183
|
+
if (!prevManifestRef.current) {
|
|
184
|
+
prevManifestRef.current = true
|
|
185
|
+
postToParent(buildReadyMessage(m, window.location.pathname))
|
|
186
|
+
} else {
|
|
187
|
+
postToParent(buildPageNavigatedMessage(m, window.location.pathname))
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Send cms-state-changed when editor state changes
|
|
192
|
+
const prevStateRef = useRef<string>('')
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const state = buildEditorState({
|
|
195
|
+
isEditing: signals.isEditing.value,
|
|
196
|
+
dirtyCount: {
|
|
197
|
+
text: signals.dirtyChangesCount.value,
|
|
198
|
+
image: signals.dirtyImageChangesCount.value,
|
|
199
|
+
color: signals.dirtyColorChangesCount.value,
|
|
200
|
+
bgImage: signals.dirtyBgImageChangesCount.value,
|
|
201
|
+
attribute: signals.dirtyAttributeChangesCount.value,
|
|
202
|
+
seo: signals.dirtySeoChangesCount.value,
|
|
203
|
+
total: signals.totalDirtyCount.value,
|
|
204
|
+
},
|
|
205
|
+
deploymentStatus: signals.deploymentStatus.value,
|
|
206
|
+
lastDeployedAt: signals.lastDeployedAt.value,
|
|
207
|
+
canUndo: canUndo.value,
|
|
208
|
+
canRedo: canRedo.value,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const key = JSON.stringify(state)
|
|
212
|
+
if (key === prevStateRef.current) return
|
|
213
|
+
prevStateRef.current = key
|
|
214
|
+
|
|
215
|
+
postToParent(buildStateChangedMessage(state))
|
|
132
216
|
})
|
|
133
217
|
|
|
134
218
|
const {
|
|
@@ -151,6 +235,7 @@ const CmsUI = () => {
|
|
|
151
235
|
hideTooltip()
|
|
152
236
|
stopEditMode(updateUI)
|
|
153
237
|
} else {
|
|
238
|
+
signals.isSelectMode.value = false
|
|
154
239
|
await startEditMode(config, updateUI)
|
|
155
240
|
}
|
|
156
241
|
}, [config, updateUI, hideTooltip])
|
|
@@ -209,6 +294,10 @@ const CmsUI = () => {
|
|
|
209
294
|
openSeoEditor()
|
|
210
295
|
}, [])
|
|
211
296
|
|
|
297
|
+
const handleSelectElementToggle = useCallback(() => {
|
|
298
|
+
signals.isSelectMode.value = !signals.isSelectMode.value
|
|
299
|
+
}, [])
|
|
300
|
+
|
|
212
301
|
// Color toolbar handlers
|
|
213
302
|
const handleColorToolbarChange = useCallback(
|
|
214
303
|
(
|
|
@@ -308,7 +397,6 @@ const CmsUI = () => {
|
|
|
308
397
|
// Get reactive values from signals
|
|
309
398
|
const isEditing = signals.isEditing.value
|
|
310
399
|
const isAIProcessing = signals.isAIProcessing.value
|
|
311
|
-
const isChatOpen = signals.isChatOpen.value
|
|
312
400
|
const blockEditorState = signals.blockEditorState.value
|
|
313
401
|
const colorEditorState = signals.colorEditorState.value
|
|
314
402
|
const manifest = signals.manifest.value
|
|
@@ -384,7 +472,7 @@ const CmsUI = () => {
|
|
|
384
472
|
onCompare: handleCompare,
|
|
385
473
|
onSave: handleSave,
|
|
386
474
|
onDiscard: handleDiscard,
|
|
387
|
-
|
|
475
|
+
onSelectElement: handleSelectElementToggle,
|
|
388
476
|
onMediaLibrary: handleMediaLibrary,
|
|
389
477
|
onDismissDeployment: handleDismissDeployment,
|
|
390
478
|
onNavigateChange: () => {
|
|
@@ -400,18 +488,6 @@ const CmsUI = () => {
|
|
|
400
488
|
/>
|
|
401
489
|
</ErrorBoundary>
|
|
402
490
|
|
|
403
|
-
<ErrorBoundary componentName="AI Tooltip">
|
|
404
|
-
<AITooltip
|
|
405
|
-
callbacks={{
|
|
406
|
-
onPromptSubmit: handleTooltipPromptSubmit,
|
|
407
|
-
}}
|
|
408
|
-
visible={!!tooltipState.elementId && isEditing && !isAIProcessing && !textSelectionState.hasSelection}
|
|
409
|
-
elementId={tooltipState.elementId}
|
|
410
|
-
rect={tooltipState.rect}
|
|
411
|
-
processing={isAIProcessing}
|
|
412
|
-
/>
|
|
413
|
-
</ErrorBoundary>
|
|
414
|
-
|
|
415
491
|
<ErrorBoundary componentName="Text Style Toolbar">
|
|
416
492
|
<TextStyleToolbar
|
|
417
493
|
visible={textSelectionState.hasSelection && isEditing && !isAIProcessing && isTextStylingAllowed}
|
|
@@ -439,17 +515,6 @@ const CmsUI = () => {
|
|
|
439
515
|
/>
|
|
440
516
|
</ErrorBoundary>
|
|
441
517
|
|
|
442
|
-
<ErrorBoundary componentName="AI Chat">
|
|
443
|
-
<AIChat
|
|
444
|
-
callbacks={{
|
|
445
|
-
onSend: handleChatSend,
|
|
446
|
-
onClose: handleChatClose,
|
|
447
|
-
onCancel: handleChatCancel,
|
|
448
|
-
onApplyToElement: handleApplyToElement,
|
|
449
|
-
}}
|
|
450
|
-
/>
|
|
451
|
-
</ErrorBoundary>
|
|
452
|
-
|
|
453
518
|
<ErrorBoundary componentName="Block Editor">
|
|
454
519
|
<BlockEditor
|
|
455
520
|
visible={blockEditorState.isOpen && isEditing}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CmsEditorState,
|
|
3
|
+
CmsManifest,
|
|
4
|
+
CmsPageNavigatedMessage,
|
|
5
|
+
CmsPostMessage,
|
|
6
|
+
CmsReadyMessage,
|
|
7
|
+
CmsSelectedElement,
|
|
8
|
+
CmsStateChangedMessage,
|
|
9
|
+
ManifestEntry,
|
|
10
|
+
PageSeoData,
|
|
11
|
+
} from '../types'
|
|
12
|
+
import type { ComponentInstance } from '../types'
|
|
13
|
+
|
|
14
|
+
/** Send a postMessage to the parent window (no-op when not in an iframe) */
|
|
15
|
+
export function postToParent(msg: CmsPostMessage): void {
|
|
16
|
+
if (window.parent !== window) {
|
|
17
|
+
window.parent.postMessage(msg, '*')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Build a CmsSelectedElement from manifest data and outline state */
|
|
22
|
+
export function buildSelectedElement(opts: {
|
|
23
|
+
cmsId: string | null
|
|
24
|
+
isComponent: boolean
|
|
25
|
+
componentName?: string
|
|
26
|
+
componentId?: string
|
|
27
|
+
tagName?: string
|
|
28
|
+
rect: { x: number; y: number; width: number; height: number } | null
|
|
29
|
+
entry?: ManifestEntry
|
|
30
|
+
instance?: ComponentInstance
|
|
31
|
+
}): CmsSelectedElement {
|
|
32
|
+
const { cmsId, isComponent, componentName, componentId, tagName, rect, entry, instance } = opts
|
|
33
|
+
return {
|
|
34
|
+
cmsId,
|
|
35
|
+
isComponent,
|
|
36
|
+
componentName: componentName ?? instance?.componentName,
|
|
37
|
+
componentId,
|
|
38
|
+
tagName: tagName ?? entry?.tag,
|
|
39
|
+
rect,
|
|
40
|
+
...(entry && {
|
|
41
|
+
text: entry.text,
|
|
42
|
+
html: entry.html,
|
|
43
|
+
sourcePath: entry.sourcePath,
|
|
44
|
+
sourceLine: entry.sourceLine,
|
|
45
|
+
sourceSnippet: entry.sourceSnippet,
|
|
46
|
+
sourceHash: entry.sourceHash,
|
|
47
|
+
stableId: entry.stableId,
|
|
48
|
+
contentPath: entry.contentPath,
|
|
49
|
+
parentComponentId: entry.parentComponentId,
|
|
50
|
+
childCmsIds: entry.childCmsIds,
|
|
51
|
+
imageMetadata: entry.imageMetadata,
|
|
52
|
+
backgroundImage: entry.backgroundImage,
|
|
53
|
+
colorClasses: entry.colorClasses,
|
|
54
|
+
attributes: entry.attributes,
|
|
55
|
+
constraints: entry.constraints,
|
|
56
|
+
allowStyling: entry.allowStyling,
|
|
57
|
+
collectionName: entry.collectionName,
|
|
58
|
+
collectionSlug: entry.collectionSlug,
|
|
59
|
+
}),
|
|
60
|
+
...(instance && {
|
|
61
|
+
component: {
|
|
62
|
+
name: instance.componentName,
|
|
63
|
+
file: instance.file,
|
|
64
|
+
sourcePath: instance.sourcePath,
|
|
65
|
+
sourceLine: instance.sourceLine,
|
|
66
|
+
props: instance.props,
|
|
67
|
+
slots: instance.slots,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Build a CmsReadyMessage from the loaded manifest */
|
|
74
|
+
export function buildReadyMessage(manifest: CmsManifest, pathname: string): CmsReadyMessage {
|
|
75
|
+
const seo = (manifest as any).seo as PageSeoData | undefined
|
|
76
|
+
const pageTitle = seo?.title?.content ?? manifest.pages?.find(p => p.pathname === pathname)?.title
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
type: 'cms-ready',
|
|
80
|
+
data: {
|
|
81
|
+
pathname,
|
|
82
|
+
pageTitle,
|
|
83
|
+
seo,
|
|
84
|
+
pages: manifest.pages,
|
|
85
|
+
collectionDefinitions: manifest.collectionDefinitions,
|
|
86
|
+
componentDefinitions: manifest.componentDefinitions,
|
|
87
|
+
availableColors: manifest.availableColors,
|
|
88
|
+
availableTextStyles: manifest.availableTextStyles,
|
|
89
|
+
metadata: manifest.metadata,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Build a CmsPageNavigatedMessage */
|
|
95
|
+
export function buildPageNavigatedMessage(manifest: CmsManifest, pathname: string): CmsPageNavigatedMessage {
|
|
96
|
+
const seo = (manifest as any).seo as PageSeoData | undefined
|
|
97
|
+
const pageTitle = seo?.title?.content ?? manifest.pages?.find(p => p.pathname === pathname)?.title
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
type: 'cms-page-navigated',
|
|
101
|
+
page: {
|
|
102
|
+
pathname,
|
|
103
|
+
title: pageTitle,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Build a CmsEditorState snapshot from current signal values */
|
|
109
|
+
export function buildEditorState(opts: {
|
|
110
|
+
isEditing: boolean
|
|
111
|
+
dirtyCount: CmsEditorState['dirtyCount']
|
|
112
|
+
deploymentStatus: CmsEditorState['deployment']['status']
|
|
113
|
+
lastDeployedAt: string | null
|
|
114
|
+
canUndo: boolean
|
|
115
|
+
canRedo: boolean
|
|
116
|
+
}): CmsEditorState {
|
|
117
|
+
return {
|
|
118
|
+
isEditing: opts.isEditing,
|
|
119
|
+
hasChanges: opts.dirtyCount.total > 0,
|
|
120
|
+
dirtyCount: opts.dirtyCount,
|
|
121
|
+
deployment: {
|
|
122
|
+
status: opts.deploymentStatus,
|
|
123
|
+
lastDeployedAt: opts.lastDeployedAt,
|
|
124
|
+
},
|
|
125
|
+
canUndo: opts.canUndo,
|
|
126
|
+
canRedo: opts.canRedo,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Build a CmsStateChangedMessage */
|
|
131
|
+
export function buildStateChangedMessage(state: CmsEditorState): CmsStateChangedMessage {
|
|
132
|
+
return {
|
|
133
|
+
type: 'cms-state-changed',
|
|
134
|
+
state,
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/editor/signals.ts
CHANGED
|
@@ -213,6 +213,7 @@ function createInitialAttributeEditorState(): AttributeEditorState {
|
|
|
213
213
|
|
|
214
214
|
export const isEnabled = signal(false)
|
|
215
215
|
export const isEditing = signal(false)
|
|
216
|
+
export const isSelectMode = signal(false)
|
|
216
217
|
export const isSaving = signal(false)
|
|
217
218
|
export const showingOriginal = signal(false)
|
|
218
219
|
export const currentEditingId = signal<string | null>(null)
|
package/src/index.ts
CHANGED
|
@@ -287,8 +287,17 @@ export type {
|
|
|
287
287
|
AvailableColors,
|
|
288
288
|
AvailableTextStyles,
|
|
289
289
|
CanonicalUrl,
|
|
290
|
+
CmsEditorState,
|
|
291
|
+
CmsElementDeselectedMessage,
|
|
292
|
+
CmsElementSelectedMessage,
|
|
290
293
|
CmsManifest,
|
|
291
294
|
CmsMarkerOptions,
|
|
295
|
+
CmsPageNavigatedMessage,
|
|
296
|
+
CmsPostMessage,
|
|
297
|
+
CmsReadyData,
|
|
298
|
+
CmsReadyMessage,
|
|
299
|
+
CmsSelectedElement,
|
|
300
|
+
CmsStateChangedMessage,
|
|
292
301
|
CollectionDefinition,
|
|
293
302
|
CollectionEntry,
|
|
294
303
|
ComponentDefinition,
|
package/src/types.ts
CHANGED
|
@@ -423,3 +423,148 @@ export interface PageSeoData {
|
|
|
423
423
|
/** JSON-LD structured data blocks */
|
|
424
424
|
jsonLd?: JsonLdEntry[]
|
|
425
425
|
}
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// PostMessage Types (iframe communication)
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
/** Element data sent to parent when a CMS element is hovered/selected */
|
|
432
|
+
export interface CmsSelectedElement {
|
|
433
|
+
/** CMS element ID (null for component-only selections) */
|
|
434
|
+
cmsId: string | null
|
|
435
|
+
/** Whether the selected element is a component root */
|
|
436
|
+
isComponent: boolean
|
|
437
|
+
/** Component name if applicable */
|
|
438
|
+
componentName?: string
|
|
439
|
+
/** Component instance ID */
|
|
440
|
+
componentId?: string
|
|
441
|
+
/** HTML tag name */
|
|
442
|
+
tagName?: string
|
|
443
|
+
/** Bounding rect relative to the iframe viewport */
|
|
444
|
+
rect: { x: number; y: number; width: number; height: number } | null
|
|
445
|
+
|
|
446
|
+
// --- Manifest entry data (text/image elements) ---
|
|
447
|
+
|
|
448
|
+
/** Plain text content */
|
|
449
|
+
text?: string
|
|
450
|
+
/** HTML content with inline styling */
|
|
451
|
+
html?: string
|
|
452
|
+
/** Source file path */
|
|
453
|
+
sourcePath?: string
|
|
454
|
+
/** Line number in source file */
|
|
455
|
+
sourceLine?: number
|
|
456
|
+
/** Parent component ID */
|
|
457
|
+
parentComponentId?: string
|
|
458
|
+
/** Nested CMS element IDs */
|
|
459
|
+
childCmsIds?: string[]
|
|
460
|
+
/** Image metadata for img elements */
|
|
461
|
+
imageMetadata?: ImageMetadata
|
|
462
|
+
/** Background image metadata */
|
|
463
|
+
backgroundImage?: BackgroundImageMetadata
|
|
464
|
+
/** Color classes (bg, text, border, etc.) */
|
|
465
|
+
colorClasses?: Record<string, Attribute>
|
|
466
|
+
/** HTML attributes with source info */
|
|
467
|
+
attributes?: Record<string, Attribute>
|
|
468
|
+
/** Content validation constraints */
|
|
469
|
+
constraints?: ContentConstraints
|
|
470
|
+
/** Whether inline text styling is allowed */
|
|
471
|
+
allowStyling?: boolean
|
|
472
|
+
/** Collection name if from a content collection */
|
|
473
|
+
collectionName?: string
|
|
474
|
+
/** Collection entry slug */
|
|
475
|
+
collectionSlug?: string
|
|
476
|
+
/** Full element snippet from source */
|
|
477
|
+
sourceSnippet?: string
|
|
478
|
+
/** SHA256 hash of sourceSnippet for conflict detection */
|
|
479
|
+
sourceHash?: string
|
|
480
|
+
/** Stable ID derived from content + context hash */
|
|
481
|
+
stableId?: string
|
|
482
|
+
/** Path to the markdown content file */
|
|
483
|
+
contentPath?: string
|
|
484
|
+
|
|
485
|
+
// --- Component instance data ---
|
|
486
|
+
|
|
487
|
+
/** Full component instance info (when isComponent is true) */
|
|
488
|
+
component?: {
|
|
489
|
+
name: string
|
|
490
|
+
file: string
|
|
491
|
+
sourcePath: string
|
|
492
|
+
sourceLine: number
|
|
493
|
+
props: Record<string, unknown>
|
|
494
|
+
slots?: Record<string, string>
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Message sent when a CMS element is hovered/selected */
|
|
499
|
+
export interface CmsElementSelectedMessage {
|
|
500
|
+
type: 'cms-element-selected'
|
|
501
|
+
element: CmsSelectedElement
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Message sent when no element is hovered */
|
|
505
|
+
export interface CmsElementDeselectedMessage {
|
|
506
|
+
type: 'cms-element-deselected'
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Data sent with the cms-ready message when the manifest first loads */
|
|
510
|
+
export interface CmsReadyData {
|
|
511
|
+
pathname: string
|
|
512
|
+
pageTitle?: string
|
|
513
|
+
seo?: PageSeoData
|
|
514
|
+
pages?: PageEntry[]
|
|
515
|
+
collectionDefinitions?: Record<string, CollectionDefinition>
|
|
516
|
+
componentDefinitions?: Record<string, ComponentDefinition>
|
|
517
|
+
availableColors?: AvailableColors
|
|
518
|
+
availableTextStyles?: AvailableTextStyles
|
|
519
|
+
metadata?: ManifestMetadata
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Message sent when the CMS manifest has loaded and the editor is ready */
|
|
523
|
+
export interface CmsReadyMessage {
|
|
524
|
+
type: 'cms-ready'
|
|
525
|
+
data: CmsReadyData
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Snapshot of editor state sent on every meaningful change */
|
|
529
|
+
export interface CmsEditorState {
|
|
530
|
+
isEditing: boolean
|
|
531
|
+
hasChanges: boolean
|
|
532
|
+
dirtyCount: {
|
|
533
|
+
text: number
|
|
534
|
+
image: number
|
|
535
|
+
color: number
|
|
536
|
+
bgImage: number
|
|
537
|
+
attribute: number
|
|
538
|
+
seo: number
|
|
539
|
+
total: number
|
|
540
|
+
}
|
|
541
|
+
deployment: {
|
|
542
|
+
status: 'pending' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled' | null
|
|
543
|
+
lastDeployedAt: string | null
|
|
544
|
+
}
|
|
545
|
+
canUndo: boolean
|
|
546
|
+
canRedo: boolean
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Message sent when editor state changes (dirty counts, deployment, editing mode, undo/redo) */
|
|
550
|
+
export interface CmsStateChangedMessage {
|
|
551
|
+
type: 'cms-state-changed'
|
|
552
|
+
state: CmsEditorState
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Message sent when the user navigates to a different page (manifest reload) */
|
|
556
|
+
export interface CmsPageNavigatedMessage {
|
|
557
|
+
type: 'cms-page-navigated'
|
|
558
|
+
page: {
|
|
559
|
+
pathname: string
|
|
560
|
+
title?: string
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** All possible CMS postMessage types sent from the editor iframe to the parent */
|
|
565
|
+
export type CmsPostMessage =
|
|
566
|
+
| CmsElementSelectedMessage
|
|
567
|
+
| CmsElementDeselectedMessage
|
|
568
|
+
| CmsReadyMessage
|
|
569
|
+
| CmsStateChangedMessage
|
|
570
|
+
| CmsPageNavigatedMessage
|