@nuasite/cms 0.9.3 → 0.11.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/README.md +21 -0
- package/dist/editor.js +7104 -6726
- package/package.json +1 -1
- package/src/editor/components/fields.tsx +48 -0
- package/src/editor/components/selection-highlight.tsx +213 -0
- package/src/editor/components/seo-editor.tsx +114 -17
- package/src/editor/components/toolbar.tsx +32 -16
- package/src/editor/constants.ts +9 -3
- package/src/editor/editor.ts +4 -1
- package/src/editor/hooks/useBlockEditorHandlers.ts +8 -1
- package/src/editor/hooks/useElementDetection.ts +222 -12
- package/src/editor/index.tsx +41 -4
- package/src/editor/signals.ts +14 -0
- package/src/index.ts +2 -0
- package/src/seo-processor.ts +12 -0
- package/src/types.ts +16 -0
|
@@ -3,6 +3,7 @@ import { CSS, LAYOUT, TIMING } from '../constants'
|
|
|
3
3
|
import { getCmsElementAtPosition, getComponentAtPosition, isNearElementEdge } from '../dom'
|
|
4
4
|
import { getComponentInstance } from '../manifest'
|
|
5
5
|
import * as signals from '../signals'
|
|
6
|
+
import type { SelectedElement } from '../signals'
|
|
6
7
|
import { isEventOnCmsUI, usePositionTracking } from './utils'
|
|
7
8
|
|
|
8
9
|
export interface OutlineState {
|
|
@@ -26,6 +27,114 @@ const INITIAL_STATE: OutlineState = {
|
|
|
26
27
|
cmsId: null,
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Build a component-style outline state for a given component element.
|
|
32
|
+
*/
|
|
33
|
+
function buildComponentOutline(componentEl: HTMLElement, manifest: any): OutlineState {
|
|
34
|
+
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
35
|
+
const instance = componentId ? getComponentInstance(manifest, componentId) : null
|
|
36
|
+
return {
|
|
37
|
+
visible: true,
|
|
38
|
+
rect: componentEl.getBoundingClientRect(),
|
|
39
|
+
isComponent: true,
|
|
40
|
+
componentName: instance?.componentName,
|
|
41
|
+
tagName: componentEl.tagName.toLowerCase(),
|
|
42
|
+
element: componentEl,
|
|
43
|
+
cmsId: null,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if an element is currently selected (either as component or select-mode element).
|
|
49
|
+
*/
|
|
50
|
+
function isElementSelected(el: HTMLElement): boolean {
|
|
51
|
+
// Check component selection
|
|
52
|
+
const componentId = el.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
53
|
+
if (componentId && componentId === signals.currentComponentId.value) return true
|
|
54
|
+
|
|
55
|
+
// Check select-mode selection
|
|
56
|
+
const selectEl = signals.selectModeElement.value
|
|
57
|
+
if (selectEl && selectEl.element === el) return true
|
|
58
|
+
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a CMS element to a SelectedElement descriptor for select mode.
|
|
64
|
+
*/
|
|
65
|
+
function resolveSelectedElement(el: HTMLElement, manifest: any): SelectedElement | null {
|
|
66
|
+
// Component element
|
|
67
|
+
const componentId = el.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
68
|
+
if (componentId) {
|
|
69
|
+
const instance = getComponentInstance(manifest, componentId)
|
|
70
|
+
return {
|
|
71
|
+
element: el,
|
|
72
|
+
id: componentId,
|
|
73
|
+
label: instance?.componentName ?? el.tagName.toLowerCase(),
|
|
74
|
+
type: 'component',
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Image element
|
|
79
|
+
if (el.hasAttribute('data-cms-img')) {
|
|
80
|
+
const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE) ?? el.getAttribute('data-cms-img') ?? ''
|
|
81
|
+
return {
|
|
82
|
+
element: el,
|
|
83
|
+
id: cmsId,
|
|
84
|
+
label: el.getAttribute('alt') || 'Image',
|
|
85
|
+
type: 'image',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Background image element
|
|
90
|
+
if (el.hasAttribute('data-cms-bg-img')) {
|
|
91
|
+
const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE) ?? el.getAttribute('data-cms-bg-img') ?? ''
|
|
92
|
+
return {
|
|
93
|
+
element: el,
|
|
94
|
+
id: cmsId,
|
|
95
|
+
label: 'Background Image',
|
|
96
|
+
type: 'image',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Text/CMS element
|
|
101
|
+
const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
|
|
102
|
+
if (cmsId) {
|
|
103
|
+
const tagName = el.tagName.toLowerCase()
|
|
104
|
+
return {
|
|
105
|
+
element: el,
|
|
106
|
+
id: cmsId,
|
|
107
|
+
label: tagName,
|
|
108
|
+
type: 'text',
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Find any CMS element at position (text, image, bg-image, or component).
|
|
117
|
+
* Returns the deepest/most specific CMS element at the given point.
|
|
118
|
+
*/
|
|
119
|
+
function getAnyCmsElementAtPosition(x: number, y: number): HTMLElement | null {
|
|
120
|
+
const elementsAtPoint = document.elementsFromPoint(x, y)
|
|
121
|
+
for (const el of elementsAtPoint) {
|
|
122
|
+
if (!(el instanceof HTMLElement)) continue
|
|
123
|
+
// Skip CMS UI elements
|
|
124
|
+
if (el.hasAttribute(CSS.UI_ATTRIBUTE) || el.closest(`[${CSS.UI_ATTRIBUTE}]`)) continue
|
|
125
|
+
// Any element with a CMS attribute
|
|
126
|
+
if (
|
|
127
|
+
el.hasAttribute(CSS.ID_ATTRIBUTE)
|
|
128
|
+
|| el.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
129
|
+
|| el.hasAttribute('data-cms-img')
|
|
130
|
+
|| el.hasAttribute('data-cms-bg-img')
|
|
131
|
+
) {
|
|
132
|
+
return el
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
29
138
|
/**
|
|
30
139
|
* Hook for detecting and tracking hovered CMS elements.
|
|
31
140
|
* Uses signals directly for state management.
|
|
@@ -54,6 +163,22 @@ export function useElementDetection(): OutlineState {
|
|
|
54
163
|
outlineState.visible,
|
|
55
164
|
)
|
|
56
165
|
|
|
166
|
+
// Hide hover outline immediately when a component is selected
|
|
167
|
+
const currentComponentId = signals.currentComponentId.value
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (currentComponentId) {
|
|
170
|
+
setOutlineState(INITIAL_STATE)
|
|
171
|
+
}
|
|
172
|
+
}, [currentComponentId])
|
|
173
|
+
|
|
174
|
+
// Hide hover outline immediately when a select-mode element is selected
|
|
175
|
+
const selectModeElement = signals.selectModeElement.value
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (selectModeElement) {
|
|
178
|
+
setOutlineState(INITIAL_STATE)
|
|
179
|
+
}
|
|
180
|
+
}, [selectModeElement])
|
|
181
|
+
|
|
57
182
|
// Setup hover highlight for both elements and components
|
|
58
183
|
useEffect(() => {
|
|
59
184
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
@@ -89,10 +214,48 @@ export function useElementDetection(): OutlineState {
|
|
|
89
214
|
const manifest = signals.manifest.value
|
|
90
215
|
const entries = manifest.entries
|
|
91
216
|
|
|
217
|
+
// ── Select mode: show component-style outline for any CMS element ──
|
|
218
|
+
if (selectMode) {
|
|
219
|
+
const cmsEl = getAnyCmsElementAtPosition(ev.clientX, ev.clientY)
|
|
220
|
+
if (cmsEl) {
|
|
221
|
+
if (isElementSelected(cmsEl)) {
|
|
222
|
+
setOutlineState(INITIAL_STATE)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (hideTimeoutRef.current) {
|
|
226
|
+
clearTimeout(hideTimeoutRef.current)
|
|
227
|
+
hideTimeoutRef.current = null
|
|
228
|
+
}
|
|
229
|
+
const resolved = resolveSelectedElement(cmsEl, manifest)
|
|
230
|
+
const rect = cmsEl.getBoundingClientRect()
|
|
231
|
+
setOutlineState({
|
|
232
|
+
visible: true,
|
|
233
|
+
rect,
|
|
234
|
+
isComponent: true,
|
|
235
|
+
componentName: resolved?.label,
|
|
236
|
+
tagName: cmsEl.tagName.toLowerCase(),
|
|
237
|
+
element: cmsEl,
|
|
238
|
+
cmsId: null,
|
|
239
|
+
})
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
setOutlineState(INITIAL_STATE)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Edit mode: standard detection ──
|
|
247
|
+
|
|
92
248
|
// Use the improved elementsFromPoint-based detection
|
|
93
249
|
const cmsEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
|
|
94
250
|
|
|
95
251
|
if (cmsEl && !cmsEl.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) {
|
|
252
|
+
// Hide hover outline if this element is inside the selected component
|
|
253
|
+
const selectedId = signals.currentComponentId.value
|
|
254
|
+
if (selectedId && cmsEl.closest(`[${CSS.COMPONENT_ID_ATTRIBUTE}="${selectedId}"]`)) {
|
|
255
|
+
setOutlineState(INITIAL_STATE)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
96
259
|
// Found a text-editable element - cancel any pending hide
|
|
97
260
|
if (hideTimeoutRef.current) {
|
|
98
261
|
clearTimeout(hideTimeoutRef.current)
|
|
@@ -115,21 +278,21 @@ export function useElementDetection(): OutlineState {
|
|
|
115
278
|
// Check for component at position
|
|
116
279
|
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
117
280
|
if (componentEl) {
|
|
281
|
+
// Hide hover outline if this component is already selected
|
|
282
|
+
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
283
|
+
if (componentId && componentId === signals.currentComponentId.value) {
|
|
284
|
+
setOutlineState(INITIAL_STATE)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
118
288
|
const rect = componentEl.getBoundingClientRect()
|
|
119
|
-
const nearEdge = isNearElementEdge(
|
|
120
|
-
ev.clientX,
|
|
121
|
-
ev.clientY,
|
|
122
|
-
rect,
|
|
123
|
-
LAYOUT.COMPONENT_EDGE_THRESHOLD,
|
|
124
|
-
)
|
|
125
289
|
|
|
126
|
-
if (ev.altKey ||
|
|
290
|
+
if (ev.altKey || isNearElementEdge(ev.clientX, ev.clientY, rect, LAYOUT.COMPONENT_EDGE_THRESHOLD)) {
|
|
127
291
|
// Cancel any pending hide
|
|
128
292
|
if (hideTimeoutRef.current) {
|
|
129
293
|
clearTimeout(hideTimeoutRef.current)
|
|
130
294
|
hideTimeoutRef.current = null
|
|
131
295
|
}
|
|
132
|
-
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
133
296
|
const instance = componentId ? getComponentInstance(manifest, componentId) : null
|
|
134
297
|
|
|
135
298
|
setOutlineState({
|
|
@@ -182,6 +345,7 @@ export function useElementDetection(): OutlineState {
|
|
|
182
345
|
|
|
183
346
|
export interface ComponentClickHandlerOptions {
|
|
184
347
|
onComponentSelect: (componentId: string, cursor: { x: number; y: number }) => void
|
|
348
|
+
onComponentDeselect?: () => void
|
|
185
349
|
}
|
|
186
350
|
|
|
187
351
|
/**
|
|
@@ -190,6 +354,7 @@ export interface ComponentClickHandlerOptions {
|
|
|
190
354
|
*/
|
|
191
355
|
export function useComponentClickHandler({
|
|
192
356
|
onComponentSelect,
|
|
357
|
+
onComponentDeselect,
|
|
193
358
|
}: ComponentClickHandlerOptions): void {
|
|
194
359
|
useEffect(() => {
|
|
195
360
|
const handleClick = (ev: MouseEvent) => {
|
|
@@ -203,17 +368,39 @@ export function useComponentClickHandler({
|
|
|
203
368
|
const manifest = signals.manifest.value
|
|
204
369
|
const entries = manifest.entries
|
|
205
370
|
|
|
206
|
-
//
|
|
371
|
+
// ── Select mode: select any CMS element ──
|
|
372
|
+
if (selectMode) {
|
|
373
|
+
const cmsEl = getAnyCmsElementAtPosition(ev.clientX, ev.clientY)
|
|
374
|
+
if (cmsEl) {
|
|
375
|
+
const resolved = resolveSelectedElement(cmsEl, manifest)
|
|
376
|
+
if (resolved) {
|
|
377
|
+
ev.preventDefault()
|
|
378
|
+
ev.stopPropagation()
|
|
379
|
+
signals.setSelectModeElement(resolved)
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Clicking empty space deselects
|
|
384
|
+
signals.setSelectModeElement(null)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Edit mode: standard behavior ──
|
|
389
|
+
|
|
207
390
|
// Check for text element first
|
|
208
391
|
const textEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
|
|
209
392
|
if (textEl && !textEl.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) {
|
|
210
|
-
//
|
|
393
|
+
// In edit mode, clicking a text element deselects any selected component
|
|
394
|
+
if (signals.currentComponentId.value && onComponentDeselect) {
|
|
395
|
+
onComponentDeselect()
|
|
396
|
+
}
|
|
211
397
|
return
|
|
212
398
|
}
|
|
213
399
|
|
|
214
400
|
// Check for component click
|
|
215
401
|
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
216
402
|
if (componentEl) {
|
|
403
|
+
// In edit mode, require edge proximity or Alt key
|
|
217
404
|
const rect = componentEl.getBoundingClientRect()
|
|
218
405
|
const nearEdge = isNearElementEdge(
|
|
219
406
|
ev.clientX,
|
|
@@ -229,13 +416,36 @@ export function useComponentClickHandler({
|
|
|
229
416
|
ev.stopPropagation()
|
|
230
417
|
onComponentSelect(componentId, { x: ev.clientX, y: ev.clientY })
|
|
231
418
|
}
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Clicking on empty space deselects any selected component
|
|
424
|
+
if (signals.currentComponentId.value && onComponentDeselect) {
|
|
425
|
+
onComponentDeselect()
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Escape key deselects
|
|
430
|
+
const handleKeyDown = (ev: KeyboardEvent) => {
|
|
431
|
+
if (ev.key === 'Escape') {
|
|
432
|
+
if (signals.selectModeElement.value) {
|
|
433
|
+
ev.preventDefault()
|
|
434
|
+
signals.setSelectModeElement(null)
|
|
435
|
+
} else if (signals.currentComponentId.value && onComponentDeselect) {
|
|
436
|
+
ev.preventDefault()
|
|
437
|
+
onComponentDeselect()
|
|
232
438
|
}
|
|
233
439
|
}
|
|
234
440
|
}
|
|
235
441
|
|
|
236
442
|
document.addEventListener('click', handleClick, true)
|
|
237
|
-
|
|
238
|
-
|
|
443
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
444
|
+
return () => {
|
|
445
|
+
document.removeEventListener('click', handleClick, true)
|
|
446
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
447
|
+
}
|
|
448
|
+
}, [onComponentSelect, onComponentDeselect])
|
|
239
449
|
}
|
|
240
450
|
|
|
241
451
|
// Re-export utilities for backwards compatibility
|
package/src/editor/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { render } from 'preact'
|
|
2
2
|
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
|
3
|
-
import type { CmsElementDeselectedMessage, CmsElementSelectedMessage } from '../types'
|
|
3
|
+
import type { CmsElementDeselectedMessage, CmsElementSelectedMessage, CmsInboundMessage } from '../types'
|
|
4
4
|
import { fetchManifest } from './api'
|
|
5
5
|
import { AttributeEditor } from './components/attribute-editor'
|
|
6
6
|
import { BgImageOverlay } from './components/bg-image-overlay'
|
|
@@ -16,12 +16,13 @@ import { MarkdownEditorOverlay } from './components/markdown-editor-overlay'
|
|
|
16
16
|
import { MediaLibrary } from './components/media-library'
|
|
17
17
|
import { Outline } from './components/outline'
|
|
18
18
|
import { RedirectCountdown } from './components/redirect-countdown'
|
|
19
|
+
import { SelectionHighlight } from './components/selection-highlight'
|
|
19
20
|
import { SeoEditor } from './components/seo-editor'
|
|
20
21
|
import { TextStyleToolbar } from './components/text-style-toolbar'
|
|
21
22
|
import { ToastContainer } from './components/toast/toast-container'
|
|
22
23
|
import { Toolbar } from './components/toolbar'
|
|
23
24
|
import { getConfig } from './config'
|
|
24
|
-
import { logDebug } from './dom'
|
|
25
|
+
import { disableAllInteractiveElements, enableAllInteractiveElements, logDebug } from './dom'
|
|
25
26
|
import {
|
|
26
27
|
discardAllChanges,
|
|
27
28
|
dismissDeploymentStatus,
|
|
@@ -227,7 +228,25 @@ const CmsUI = () => {
|
|
|
227
228
|
showToast: signals.showToast,
|
|
228
229
|
})
|
|
229
230
|
|
|
230
|
-
useComponentClickHandler({
|
|
231
|
+
useComponentClickHandler({
|
|
232
|
+
onComponentSelect: handleComponentSelect,
|
|
233
|
+
onComponentDeselect: handleBlockEditorClose,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// Listen for inbound messages from parent window (when inside an iframe)
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const handleMessage = (ev: MessageEvent) => {
|
|
239
|
+
const msg = ev.data as CmsInboundMessage
|
|
240
|
+
if (!msg || typeof msg.type !== 'string') return
|
|
241
|
+
|
|
242
|
+
if (msg.type === 'cms-deselect-element') {
|
|
243
|
+
handleBlockEditorClose()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
window.addEventListener('message', handleMessage)
|
|
248
|
+
return () => window.removeEventListener('message', handleMessage)
|
|
249
|
+
}, [handleBlockEditorClose])
|
|
231
250
|
|
|
232
251
|
// Editor control handlers
|
|
233
252
|
const handleEditToggle = useCallback(async () => {
|
|
@@ -295,7 +314,21 @@ const CmsUI = () => {
|
|
|
295
314
|
}, [])
|
|
296
315
|
|
|
297
316
|
const handleSelectElementToggle = useCallback(() => {
|
|
298
|
-
|
|
317
|
+
const entering = !signals.isSelectMode.value
|
|
318
|
+
signals.isSelectMode.value = entering
|
|
319
|
+
// Clear select-mode selection when leaving
|
|
320
|
+
if (!entering) {
|
|
321
|
+
signals.setSelectModeElement(null)
|
|
322
|
+
}
|
|
323
|
+
// Disable/enable links and interactive elements in select mode
|
|
324
|
+
// (skip if already in edit mode, which handles its own disabling)
|
|
325
|
+
if (!signals.isEditing.value) {
|
|
326
|
+
if (entering) {
|
|
327
|
+
disableAllInteractiveElements()
|
|
328
|
+
} else {
|
|
329
|
+
enableAllInteractiveElements()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
299
332
|
}, [])
|
|
300
333
|
|
|
301
334
|
// Color toolbar handlers
|
|
@@ -447,6 +480,10 @@ const CmsUI = () => {
|
|
|
447
480
|
/>
|
|
448
481
|
</ErrorBoundary>
|
|
449
482
|
|
|
483
|
+
<ErrorBoundary componentName="Selection Highlight">
|
|
484
|
+
<SelectionHighlight />
|
|
485
|
+
</ErrorBoundary>
|
|
486
|
+
|
|
450
487
|
<ErrorBoundary componentName="ImageOverlay">
|
|
451
488
|
<ImageOverlay
|
|
452
489
|
visible={imageHoverState.visible && isEditing}
|
package/src/editor/signals.ts
CHANGED
|
@@ -219,6 +219,20 @@ export const showingOriginal = signal(false)
|
|
|
219
219
|
export const currentEditingId = signal<string | null>(null)
|
|
220
220
|
export const currentComponentId = signal<string | null>(null)
|
|
221
221
|
|
|
222
|
+
/** Selected element in select mode (any CMS element — text, image, or component) */
|
|
223
|
+
export interface SelectedElement {
|
|
224
|
+
element: HTMLElement
|
|
225
|
+
/** data-cms-id, data-cms-component-id, or data-cms-img value */
|
|
226
|
+
id: string
|
|
227
|
+
label: string
|
|
228
|
+
type: 'text' | 'image' | 'component'
|
|
229
|
+
}
|
|
230
|
+
export const selectModeElement = signal<SelectedElement | null>(null)
|
|
231
|
+
|
|
232
|
+
export function setSelectModeElement(el: SelectedElement | null): void {
|
|
233
|
+
selectModeElement.value = el
|
|
234
|
+
}
|
|
235
|
+
|
|
222
236
|
// Complex state - use signals wrapping the full object for atomicity
|
|
223
237
|
export const pendingChanges = signal<Map<string, PendingChange>>(new Map())
|
|
224
238
|
export const pendingComponentChanges = signal<Map<string, ComponentInstance>>(
|
package/src/index.ts
CHANGED
|
@@ -287,9 +287,11 @@ export type {
|
|
|
287
287
|
AvailableColors,
|
|
288
288
|
AvailableTextStyles,
|
|
289
289
|
CanonicalUrl,
|
|
290
|
+
CmsDeselectElementMessage,
|
|
290
291
|
CmsEditorState,
|
|
291
292
|
CmsElementDeselectedMessage,
|
|
292
293
|
CmsElementSelectedMessage,
|
|
294
|
+
CmsInboundMessage,
|
|
293
295
|
CmsManifest,
|
|
294
296
|
CmsMarkerOptions,
|
|
295
297
|
CmsPageNavigatedMessage,
|
package/src/seo-processor.ts
CHANGED
|
@@ -215,6 +215,18 @@ function categorizeMetaTags(metaTags: SeoMetaTag[], seo: PageSeoData): void {
|
|
|
215
215
|
continue
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Theme color
|
|
219
|
+
if (name === 'theme-color') {
|
|
220
|
+
seo.themeColor = meta
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Robots
|
|
225
|
+
if (name === 'robots') {
|
|
226
|
+
seo.robots = meta
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
|
|
218
230
|
// Open Graph tags
|
|
219
231
|
if (property?.startsWith('og:')) {
|
|
220
232
|
const ogKey = property.replace('og:', '')
|
package/src/types.ts
CHANGED
|
@@ -420,6 +420,10 @@ export interface PageSeoData {
|
|
|
420
420
|
openGraph?: OpenGraphData
|
|
421
421
|
/** Twitter Card metadata */
|
|
422
422
|
twitterCard?: TwitterCardData
|
|
423
|
+
/** Browser theme color (meta name="theme-color") */
|
|
424
|
+
themeColor?: SeoMetaTag
|
|
425
|
+
/** Robots directives (meta name="robots") */
|
|
426
|
+
robots?: SeoMetaTag
|
|
423
427
|
/** JSON-LD structured data blocks */
|
|
424
428
|
jsonLd?: JsonLdEntry[]
|
|
425
429
|
}
|
|
@@ -568,3 +572,15 @@ export type CmsPostMessage =
|
|
|
568
572
|
| CmsReadyMessage
|
|
569
573
|
| CmsStateChangedMessage
|
|
570
574
|
| CmsPageNavigatedMessage
|
|
575
|
+
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// Inbound messages (parent → editor iframe)
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
580
|
+
/** Message sent from parent to deselect the currently selected element/component */
|
|
581
|
+
export interface CmsDeselectElementMessage {
|
|
582
|
+
type: 'cms-deselect-element'
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** All possible CMS postMessage types sent from the parent to the editor iframe */
|
|
586
|
+
export type CmsInboundMessage = CmsDeselectElementMessage
|