@nuasite/cms 0.7.2 → 0.8.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 +1 -5
- package/dist/editor.js +8825 -8384
- package/package.json +1 -1
- package/src/color-patterns.ts +68 -1
- package/src/editor/color-utils.ts +8 -8
- package/src/editor/components/bg-image-overlay.tsx +456 -0
- package/src/editor/components/editable-highlights.tsx +14 -0
- package/src/editor/components/frontmatter-fields.tsx +7 -3
- package/src/editor/components/outline.tsx +3 -2
- package/src/editor/components/text-style-toolbar.tsx +4 -1
- package/src/editor/constants.ts +3 -0
- package/src/editor/editor.ts +170 -2
- package/src/editor/hooks/index.ts +3 -0
- package/src/editor/hooks/useBgImageHoverDetection.ts +101 -0
- package/src/editor/index.tsx +12 -0
- package/src/editor/signals.ts +21 -2
- package/src/editor/storage.ts +50 -0
- package/src/editor/types.ts +52 -1
- package/src/handlers/source-writer.ts +30 -14
- package/src/html-processor.ts +83 -7
- package/src/source-finder/snippet-utils.ts +6 -2
- package/src/tailwind-colors.ts +1 -0
- package/src/types.ts +16 -0
package/src/editor/editor.ts
CHANGED
|
@@ -20,11 +20,13 @@ import * as signals from './signals'
|
|
|
20
20
|
import {
|
|
21
21
|
clearAllEditsFromStorage,
|
|
22
22
|
loadAttributeEditsFromStorage,
|
|
23
|
+
loadBgImageEditsFromStorage,
|
|
23
24
|
loadColorEditsFromStorage,
|
|
24
25
|
loadEditsFromStorage,
|
|
25
26
|
loadImageEditsFromStorage,
|
|
26
27
|
loadPendingEntryNavigation,
|
|
27
28
|
saveAttributeEditsToStorage,
|
|
29
|
+
saveBgImageEditsToStorage,
|
|
28
30
|
saveColorEditsToStorage,
|
|
29
31
|
saveEditingState,
|
|
30
32
|
saveEditsToStorage,
|
|
@@ -37,6 +39,8 @@ import type { AttributeChangePayload, ChangePayload, CmsConfig, DeploymentStatus
|
|
|
37
39
|
const MARKDOWN_ATTRIBUTE = 'data-cms-markdown'
|
|
38
40
|
// CSS attribute for image elements
|
|
39
41
|
const IMAGE_ATTRIBUTE = 'data-cms-img'
|
|
42
|
+
// CSS attribute for background image elements
|
|
43
|
+
const BG_IMAGE_ATTRIBUTE = 'data-cms-bg-img'
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Inline HTML elements that indicate styled/formatted content.
|
|
@@ -112,6 +116,7 @@ export async function startEditMode(
|
|
|
112
116
|
const savedImageEdits = loadImageEditsFromStorage()
|
|
113
117
|
const savedColorEdits = loadColorEditsFromStorage()
|
|
114
118
|
const savedAttributeEdits = loadAttributeEditsFromStorage()
|
|
119
|
+
const savedBgImageEdits = loadBgImageEditsFromStorage()
|
|
115
120
|
const currentManifest = signals.manifest.value
|
|
116
121
|
|
|
117
122
|
getAllCmsElements().forEach(el => {
|
|
@@ -151,6 +156,15 @@ export async function startEditMode(
|
|
|
151
156
|
return
|
|
152
157
|
}
|
|
153
158
|
|
|
159
|
+
// Check if this is a background image element
|
|
160
|
+
// Background image elements are edited via the bg image overlay panel
|
|
161
|
+
if (el.hasAttribute(BG_IMAGE_ATTRIBUTE)) {
|
|
162
|
+
logDebug(config.debug, 'Background image element detected:', cmsId)
|
|
163
|
+
makeElementNonEditable(el)
|
|
164
|
+
setupBgImageTracking(config, el, cmsId, savedBgImageEdits[cmsId])
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
154
168
|
makeElementEditable(el)
|
|
155
169
|
|
|
156
170
|
// Suppress browser native contentEditable undo/redo (we handle it ourselves)
|
|
@@ -502,10 +516,43 @@ export function discardAllChanges(onStateChange?: () => void): void {
|
|
|
502
516
|
applyAttributesToElement(element, originalAttributes)
|
|
503
517
|
})
|
|
504
518
|
|
|
519
|
+
// Restore original background image classes
|
|
520
|
+
signals.pendingBgImageChanges.value.forEach((change) => {
|
|
521
|
+
const {
|
|
522
|
+
element,
|
|
523
|
+
originalBgImageClass,
|
|
524
|
+
newBgImageClass,
|
|
525
|
+
originalBgSize,
|
|
526
|
+
newBgSize,
|
|
527
|
+
originalBgPosition,
|
|
528
|
+
newBgPosition,
|
|
529
|
+
originalBgRepeat,
|
|
530
|
+
newBgRepeat,
|
|
531
|
+
} = change
|
|
532
|
+
const classes = element.className.split(/\s+/).filter(Boolean)
|
|
533
|
+
const newClassValues = new Set([newBgImageClass, newBgSize, newBgPosition, newBgRepeat].filter(Boolean))
|
|
534
|
+
const originalClassValues = [originalBgImageClass, originalBgSize, originalBgPosition, originalBgRepeat].filter(Boolean)
|
|
535
|
+
|
|
536
|
+
const filtered = classes.filter(c => !newClassValues.has(c))
|
|
537
|
+
for (const c of originalClassValues) {
|
|
538
|
+
if (!filtered.includes(c)) {
|
|
539
|
+
filtered.push(c)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
element.className = filtered.join(' ')
|
|
543
|
+
|
|
544
|
+
// Clear inline bg style overrides
|
|
545
|
+
element.style.backgroundImage = ''
|
|
546
|
+
element.style.backgroundSize = ''
|
|
547
|
+
element.style.backgroundPosition = ''
|
|
548
|
+
element.style.backgroundRepeat = ''
|
|
549
|
+
})
|
|
550
|
+
|
|
505
551
|
cleanupHighlightSystem()
|
|
506
552
|
signals.clearPendingChanges()
|
|
507
553
|
signals.clearPendingImageChanges()
|
|
508
554
|
signals.clearPendingColorChanges()
|
|
555
|
+
signals.clearPendingBgImageChanges()
|
|
509
556
|
signals.clearPendingAttributeChanges()
|
|
510
557
|
clearAllEditsFromStorage()
|
|
511
558
|
clearHistory()
|
|
@@ -550,12 +597,13 @@ export async function saveAllChanges(
|
|
|
550
597
|
const dirtyChanges = signals.dirtyChanges.value
|
|
551
598
|
const dirtyImageChanges = signals.dirtyImageChanges.value
|
|
552
599
|
const dirtyColorChanges = signals.dirtyColorChanges.value
|
|
600
|
+
const dirtyBgImageChanges = signals.dirtyBgImageChanges.value
|
|
553
601
|
const dirtyAttributeChanges = signals.dirtyAttributeChanges.value
|
|
554
602
|
const dirtySeoChanges = signals.dirtySeoChanges.value
|
|
555
603
|
|
|
556
604
|
if (
|
|
557
|
-
dirtyChanges.length === 0 && dirtyImageChanges.length === 0 && dirtyColorChanges.length === 0 &&
|
|
558
|
-
&& dirtySeoChanges.length === 0
|
|
605
|
+
dirtyChanges.length === 0 && dirtyImageChanges.length === 0 && dirtyColorChanges.length === 0 && dirtyBgImageChanges.length === 0
|
|
606
|
+
&& dirtyAttributeChanges.length === 0 && dirtySeoChanges.length === 0
|
|
559
607
|
) {
|
|
560
608
|
return { success: true, updated: 0 }
|
|
561
609
|
}
|
|
@@ -666,6 +714,44 @@ export async function saveAllChanges(
|
|
|
666
714
|
}
|
|
667
715
|
})
|
|
668
716
|
|
|
717
|
+
// Add background image changes to the payload
|
|
718
|
+
dirtyBgImageChanges.forEach(([cmsId, change]) => {
|
|
719
|
+
const entry = manifest.entries[cmsId]
|
|
720
|
+
const bgChanges: Array<{ oldClass: string; newClass: string; type: 'bgImage' | 'bgSize' | 'bgPosition' | 'bgRepeat' }> = []
|
|
721
|
+
|
|
722
|
+
if (change.newBgImageClass !== change.originalBgImageClass) {
|
|
723
|
+
bgChanges.push({ oldClass: change.originalBgImageClass, newClass: change.newBgImageClass, type: 'bgImage' })
|
|
724
|
+
}
|
|
725
|
+
if (change.newBgSize !== change.originalBgSize) {
|
|
726
|
+
bgChanges.push({ oldClass: change.originalBgSize, newClass: change.newBgSize, type: 'bgSize' })
|
|
727
|
+
}
|
|
728
|
+
if (change.newBgPosition !== change.originalBgPosition) {
|
|
729
|
+
bgChanges.push({ oldClass: change.originalBgPosition, newClass: change.newBgPosition, type: 'bgPosition' })
|
|
730
|
+
}
|
|
731
|
+
if (change.newBgRepeat !== change.originalBgRepeat) {
|
|
732
|
+
bgChanges.push({ oldClass: change.originalBgRepeat, newClass: change.newBgRepeat, type: 'bgRepeat' })
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
for (const bgChange of bgChanges) {
|
|
736
|
+
changes.push({
|
|
737
|
+
cmsId,
|
|
738
|
+
newValue: '',
|
|
739
|
+
originalValue: '',
|
|
740
|
+
sourcePath: entry?.sourcePath ?? '',
|
|
741
|
+
sourceLine: entry?.sourceLine ?? 0,
|
|
742
|
+
sourceSnippet: entry?.sourceSnippet ?? '',
|
|
743
|
+
colorChange: {
|
|
744
|
+
oldClass: bgChange.oldClass,
|
|
745
|
+
newClass: bgChange.newClass,
|
|
746
|
+
type: bgChange.type,
|
|
747
|
+
sourcePath: entry?.sourcePath,
|
|
748
|
+
sourceLine: entry?.sourceLine,
|
|
749
|
+
sourceSnippet: entry?.sourceSnippet,
|
|
750
|
+
},
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
})
|
|
754
|
+
|
|
669
755
|
// Add attribute changes to the payload
|
|
670
756
|
dirtyAttributeChanges.forEach(([cmsId, change]) => {
|
|
671
757
|
const { originalAttributes, newAttributes } = change
|
|
@@ -740,6 +826,18 @@ export async function saveAllChanges(
|
|
|
740
826
|
}))
|
|
741
827
|
})
|
|
742
828
|
|
|
829
|
+
// Update all dirty bg image changes to mark as saved
|
|
830
|
+
dirtyBgImageChanges.forEach(([cmsId, change]) => {
|
|
831
|
+
signals.updatePendingBgImageChange(cmsId, (c) => ({
|
|
832
|
+
...c,
|
|
833
|
+
originalBgImageClass: c.newBgImageClass,
|
|
834
|
+
originalBgSize: c.newBgSize,
|
|
835
|
+
originalBgPosition: c.newBgPosition,
|
|
836
|
+
originalBgRepeat: c.newBgRepeat,
|
|
837
|
+
isDirty: false,
|
|
838
|
+
}))
|
|
839
|
+
})
|
|
840
|
+
|
|
743
841
|
// Update all dirty attribute changes to mark as saved
|
|
744
842
|
dirtyAttributeChanges.forEach(([cmsId, change]) => {
|
|
745
843
|
signals.updatePendingAttributeChange(cmsId, (c) => ({
|
|
@@ -786,6 +884,7 @@ export async function saveAllChanges(
|
|
|
786
884
|
saveEditsToStorage(signals.pendingChanges.value)
|
|
787
885
|
saveImageEditsToStorage(signals.pendingImageChanges.value)
|
|
788
886
|
saveColorEditsToStorage(signals.pendingColorChanges.value)
|
|
887
|
+
saveBgImageEditsToStorage(signals.pendingBgImageChanges.value)
|
|
789
888
|
saveAttributeEditsToStorage(signals.pendingAttributeChanges.value)
|
|
790
889
|
throw err
|
|
791
890
|
} finally {
|
|
@@ -1026,6 +1125,75 @@ function setupColorTracking(
|
|
|
1026
1125
|
}
|
|
1027
1126
|
}
|
|
1028
1127
|
|
|
1128
|
+
/**
|
|
1129
|
+
* Initialize background image change tracking for elements with bg-[url()] classes.
|
|
1130
|
+
* Background image editing is triggered via the bg image overlay panel.
|
|
1131
|
+
*/
|
|
1132
|
+
function setupBgImageTracking(
|
|
1133
|
+
config: CmsConfig,
|
|
1134
|
+
el: HTMLElement,
|
|
1135
|
+
cmsId: string,
|
|
1136
|
+
savedEdit: import('./types').SavedBackgroundImageEdit | undefined,
|
|
1137
|
+
): void {
|
|
1138
|
+
const manifest = signals.manifest.value
|
|
1139
|
+
const entry = manifest.entries[cmsId]
|
|
1140
|
+
|
|
1141
|
+
if (!entry?.backgroundImage) {
|
|
1142
|
+
return
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
logDebug(config.debug, 'Setting up bg image tracking for:', cmsId, entry.backgroundImage)
|
|
1146
|
+
|
|
1147
|
+
if (!signals.pendingBgImageChanges.value.has(cmsId)) {
|
|
1148
|
+
if (savedEdit) {
|
|
1149
|
+
// Restore saved bg image classes on the element
|
|
1150
|
+
const classes = el.className.split(/\s+/).filter(Boolean)
|
|
1151
|
+
const originals = [savedEdit.originalBgImageClass, savedEdit.originalBgSize, savedEdit.originalBgPosition, savedEdit.originalBgRepeat].filter(
|
|
1152
|
+
Boolean,
|
|
1153
|
+
)
|
|
1154
|
+
const news = [savedEdit.newBgImageClass, savedEdit.newBgSize, savedEdit.newBgPosition, savedEdit.newBgRepeat].filter(Boolean)
|
|
1155
|
+
|
|
1156
|
+
const filtered = classes.filter(c => !originals.includes(c))
|
|
1157
|
+
for (const c of news) {
|
|
1158
|
+
if (!filtered.includes(c)) {
|
|
1159
|
+
filtered.push(c)
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
el.className = filtered.join(' ')
|
|
1163
|
+
|
|
1164
|
+
signals.setPendingBgImageChange(cmsId, {
|
|
1165
|
+
element: el,
|
|
1166
|
+
cmsId,
|
|
1167
|
+
originalBgImageClass: savedEdit.originalBgImageClass,
|
|
1168
|
+
newBgImageClass: savedEdit.newBgImageClass,
|
|
1169
|
+
originalBgSize: savedEdit.originalBgSize,
|
|
1170
|
+
newBgSize: savedEdit.newBgSize,
|
|
1171
|
+
originalBgPosition: savedEdit.originalBgPosition,
|
|
1172
|
+
newBgPosition: savedEdit.newBgPosition,
|
|
1173
|
+
originalBgRepeat: savedEdit.originalBgRepeat,
|
|
1174
|
+
newBgRepeat: savedEdit.newBgRepeat,
|
|
1175
|
+
isDirty: true,
|
|
1176
|
+
})
|
|
1177
|
+
logDebug(config.debug, 'Restored saved bg image edit:', cmsId, savedEdit)
|
|
1178
|
+
} else {
|
|
1179
|
+
const bg = entry.backgroundImage
|
|
1180
|
+
signals.setPendingBgImageChange(cmsId, {
|
|
1181
|
+
element: el,
|
|
1182
|
+
cmsId,
|
|
1183
|
+
originalBgImageClass: bg.bgImageClass,
|
|
1184
|
+
newBgImageClass: bg.bgImageClass,
|
|
1185
|
+
originalBgSize: bg.bgSize ?? '',
|
|
1186
|
+
newBgSize: bg.bgSize ?? '',
|
|
1187
|
+
originalBgPosition: bg.bgPosition ?? '',
|
|
1188
|
+
newBgPosition: bg.bgPosition ?? '',
|
|
1189
|
+
originalBgRepeat: bg.bgRepeat ?? '',
|
|
1190
|
+
newBgRepeat: bg.bgRepeat ?? '',
|
|
1191
|
+
isDirty: false,
|
|
1192
|
+
})
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1029
1197
|
/**
|
|
1030
1198
|
* Handle color change from the color toolbar.
|
|
1031
1199
|
* Called when user selects a new color.
|
|
@@ -17,3 +17,6 @@ export type { TextSelectionState } from './useTextSelection'
|
|
|
17
17
|
|
|
18
18
|
export { useImageHoverDetection } from './useImageHoverDetection'
|
|
19
19
|
export type { ImageHoverState } from './useImageHoverDetection'
|
|
20
|
+
|
|
21
|
+
export { useBgImageHoverDetection } from './useBgImageHoverDetection'
|
|
22
|
+
export type { BgImageHoverState } from './useBgImageHoverDetection'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { CSS, TIMING } from '../constants'
|
|
3
|
+
import * as signals from '../signals'
|
|
4
|
+
import { isEventOnCmsUI, usePositionTracking } from './utils'
|
|
5
|
+
|
|
6
|
+
export interface BgImageHoverState {
|
|
7
|
+
visible: boolean
|
|
8
|
+
rect: DOMRect | null
|
|
9
|
+
element: HTMLElement | null
|
|
10
|
+
cmsId: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const INITIAL_STATE: BgImageHoverState = {
|
|
14
|
+
visible: false,
|
|
15
|
+
rect: null,
|
|
16
|
+
element: null,
|
|
17
|
+
cmsId: null,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook for detecting and tracking hovered CMS background image elements.
|
|
22
|
+
* Shows a visual overlay when hovering over elements marked with data-cms-bg-img.
|
|
23
|
+
*/
|
|
24
|
+
export function useBgImageHoverDetection(): BgImageHoverState {
|
|
25
|
+
const [bgImageHoverState, setBgImageHoverState] = useState<BgImageHoverState>(INITIAL_STATE)
|
|
26
|
+
|
|
27
|
+
// Throttle ref for element detection
|
|
28
|
+
const lastDetectionTime = useRef<number>(0)
|
|
29
|
+
|
|
30
|
+
// Handle position updates on scroll/resize
|
|
31
|
+
const handlePositionChange = useCallback((rect: DOMRect | null) => {
|
|
32
|
+
if (rect) {
|
|
33
|
+
setBgImageHoverState(prev => ({ ...prev, rect }))
|
|
34
|
+
} else {
|
|
35
|
+
setBgImageHoverState(INITIAL_STATE)
|
|
36
|
+
}
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
// Track element position on scroll/resize
|
|
40
|
+
usePositionTracking(
|
|
41
|
+
bgImageHoverState.element,
|
|
42
|
+
handlePositionChange,
|
|
43
|
+
bgImageHoverState.visible,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Setup hover detection for background image elements
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const handleMouseMove = (ev: MouseEvent) => {
|
|
49
|
+
const isEditing = signals.isEditing.value
|
|
50
|
+
|
|
51
|
+
if (!isEditing) {
|
|
52
|
+
setBgImageHoverState(prev => prev.visible ? INITIAL_STATE : prev)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if hovering over CMS UI - keep current state
|
|
57
|
+
if (isEventOnCmsUI(ev)) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Throttle detection for performance
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
if (now - lastDetectionTime.current < TIMING.ELEMENT_DETECTION_THROTTLE_MS) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
lastDetectionTime.current = now
|
|
67
|
+
|
|
68
|
+
// Check if hovering over an element with data-cms-bg-img attribute
|
|
69
|
+
const elements = document.elementsFromPoint(ev.clientX, ev.clientY)
|
|
70
|
+
|
|
71
|
+
for (const el of elements) {
|
|
72
|
+
// If there's a contentEditable element above, don't show overlay
|
|
73
|
+
if (el instanceof HTMLElement && el.contentEditable === 'true') {
|
|
74
|
+
setBgImageHoverState(INITIAL_STATE)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (el instanceof HTMLElement && el.hasAttribute(CSS.BG_IMAGE_ATTRIBUTE)) {
|
|
79
|
+
const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
|
|
80
|
+
const rect = el.getBoundingClientRect()
|
|
81
|
+
|
|
82
|
+
setBgImageHoverState({
|
|
83
|
+
visible: true,
|
|
84
|
+
rect,
|
|
85
|
+
element: el,
|
|
86
|
+
cmsId,
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// No bg image element found, hide overlay
|
|
93
|
+
setBgImageHoverState(INITIAL_STATE)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
document.addEventListener('mousemove', handleMouseMove, true)
|
|
97
|
+
return () => document.removeEventListener('mousemove', handleMouseMove, true)
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
return bgImageHoverState
|
|
101
|
+
}
|
package/src/editor/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { fetchManifest } from './api'
|
|
|
4
4
|
import { AIChat } from './components/ai-chat'
|
|
5
5
|
import { AITooltip } from './components/ai-tooltip'
|
|
6
6
|
import { AttributeEditor } from './components/attribute-editor'
|
|
7
|
+
import { BgImageOverlay } from './components/bg-image-overlay'
|
|
7
8
|
import { BlockEditor } from './components/block-editor'
|
|
8
9
|
import { CollectionsBrowser } from './components/collections-browser'
|
|
9
10
|
import { ColorToolbar } from './components/color-toolbar'
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
import { performRedo, performUndo } from './history'
|
|
35
36
|
import {
|
|
36
37
|
useAIHandlers,
|
|
38
|
+
useBgImageHoverDetection,
|
|
37
39
|
useBlockEditorHandlers,
|
|
38
40
|
useComponentClickHandler,
|
|
39
41
|
useElementDetection,
|
|
@@ -79,6 +81,7 @@ const CmsUI = () => {
|
|
|
79
81
|
const config = signals.config.value
|
|
80
82
|
const outlineState = useElementDetection()
|
|
81
83
|
const imageHoverState = useImageHoverDetection()
|
|
84
|
+
const bgImageHoverState = useBgImageHoverDetection()
|
|
82
85
|
const textSelectionState = useTextSelection()
|
|
83
86
|
const { tooltipState, showTooltipForElement, hideTooltip } = useTooltipState()
|
|
84
87
|
const updateUI = useCallback(() => {
|
|
@@ -365,6 +368,15 @@ const CmsUI = () => {
|
|
|
365
368
|
/>
|
|
366
369
|
</ErrorBoundary>
|
|
367
370
|
|
|
371
|
+
<ErrorBoundary componentName="BgImageOverlay">
|
|
372
|
+
<BgImageOverlay
|
|
373
|
+
visible={bgImageHoverState.visible && isEditing}
|
|
374
|
+
rect={bgImageHoverState.rect}
|
|
375
|
+
element={bgImageHoverState.element}
|
|
376
|
+
cmsId={bgImageHoverState.cmsId}
|
|
377
|
+
/>
|
|
378
|
+
</ErrorBoundary>
|
|
379
|
+
|
|
368
380
|
<ErrorBoundary componentName="Toolbar">
|
|
369
381
|
<Toolbar
|
|
370
382
|
callbacks={{
|
package/src/editor/signals.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
MediaItem,
|
|
28
28
|
MediaLibraryState,
|
|
29
29
|
PendingAttributeChange,
|
|
30
|
+
PendingBackgroundImageChange,
|
|
30
31
|
PendingChange,
|
|
31
32
|
PendingColorChange,
|
|
32
33
|
PendingComponentInsert,
|
|
@@ -231,6 +232,9 @@ export const pendingImageChanges = signal<Map<string, PendingImageChange>>(
|
|
|
231
232
|
export const pendingColorChanges = signal<Map<string, PendingColorChange>>(
|
|
232
233
|
new Map(),
|
|
233
234
|
)
|
|
235
|
+
export const pendingBgImageChanges = signal<Map<string, PendingBackgroundImageChange>>(
|
|
236
|
+
new Map(),
|
|
237
|
+
)
|
|
234
238
|
export const manifest = signal<CmsManifest>({
|
|
235
239
|
entries: {},
|
|
236
240
|
components: {},
|
|
@@ -252,6 +256,7 @@ const _pendingComponentChangesHelpers = createMapHelpers(pendingComponentChanges
|
|
|
252
256
|
const _pendingInsertsHelpers = createMapHelpers(pendingInserts)
|
|
253
257
|
const _pendingImageChangesHelpers = createMapHelpers(pendingImageChanges)
|
|
254
258
|
const _pendingColorChangesHelpers = createMapHelpers(pendingColorChanges)
|
|
259
|
+
const _pendingBgImageChangesHelpers = createMapHelpers(pendingBgImageChanges)
|
|
255
260
|
|
|
256
261
|
// ============================================================================
|
|
257
262
|
// AI State Signals
|
|
@@ -495,6 +500,7 @@ let toastIdCounter = 0
|
|
|
495
500
|
const _pendingChangesDirty = createDirtyTracking(pendingChanges)
|
|
496
501
|
const _pendingImageChangesDirty = createDirtyTracking(pendingImageChanges)
|
|
497
502
|
const _pendingColorChangesDirty = createDirtyTracking(pendingColorChanges)
|
|
503
|
+
const _pendingBgImageChangesDirty = createDirtyTracking(pendingBgImageChanges)
|
|
498
504
|
const _pendingSeoChangesDirty = createDirtyTracking(pendingSeoChanges)
|
|
499
505
|
const _pendingAttributeChangesDirty = createDirtyTracking(pendingAttributeChanges)
|
|
500
506
|
|
|
@@ -510,6 +516,10 @@ export const dirtyColorChangesCount = _pendingColorChangesDirty.dirtyCount
|
|
|
510
516
|
export const dirtyColorChanges = _pendingColorChangesDirty.dirtyItems
|
|
511
517
|
export const hasDirtyColorChanges = _pendingColorChangesDirty.hasDirty
|
|
512
518
|
|
|
519
|
+
export const dirtyBgImageChangesCount = _pendingBgImageChangesDirty.dirtyCount
|
|
520
|
+
export const dirtyBgImageChanges = _pendingBgImageChangesDirty.dirtyItems
|
|
521
|
+
export const hasDirtyBgImageChanges = _pendingBgImageChangesDirty.hasDirty
|
|
522
|
+
|
|
513
523
|
export const dirtySeoChangesCount = _pendingSeoChangesDirty.dirtyCount
|
|
514
524
|
export const dirtySeoChanges = _pendingSeoChangesDirty.dirtyItems
|
|
515
525
|
export const hasDirtySeoChanges = _pendingSeoChangesDirty.hasDirty
|
|
@@ -520,13 +530,14 @@ export const hasDirtyAttributeChanges = _pendingAttributeChangesDirty.hasDirty
|
|
|
520
530
|
|
|
521
531
|
export const totalDirtyCount = computed(
|
|
522
532
|
() =>
|
|
523
|
-
dirtyChangesCount.value + dirtyImageChangesCount.value + dirtyColorChangesCount.value + dirtySeoChangesCount.value
|
|
533
|
+
dirtyChangesCount.value + dirtyImageChangesCount.value + dirtyColorChangesCount.value + dirtyBgImageChangesCount.value + dirtySeoChangesCount.value
|
|
524
534
|
+ dirtyAttributeChangesCount.value,
|
|
525
535
|
)
|
|
526
536
|
|
|
527
537
|
export const hasAnyDirtyChanges = computed(
|
|
528
538
|
() =>
|
|
529
|
-
hasDirtyChanges.value || hasDirtyImageChanges.value || hasDirtyColorChanges.value ||
|
|
539
|
+
hasDirtyChanges.value || hasDirtyImageChanges.value || hasDirtyColorChanges.value || hasDirtyBgImageChanges.value || hasDirtySeoChanges.value
|
|
540
|
+
|| hasDirtyAttributeChanges.value,
|
|
530
541
|
)
|
|
531
542
|
|
|
532
543
|
// Navigation index for cycling through dirty elements
|
|
@@ -615,6 +626,13 @@ export const deletePendingColorChange = _pendingColorChangesHelpers.delete
|
|
|
615
626
|
export const clearPendingColorChanges = _pendingColorChangesHelpers.clear
|
|
616
627
|
export const getPendingColorChange = _pendingColorChangesHelpers.get
|
|
617
628
|
|
|
629
|
+
// Background image changes mutations - using helpers
|
|
630
|
+
export const setPendingBgImageChange = _pendingBgImageChangesHelpers.set
|
|
631
|
+
export const updatePendingBgImageChange = _pendingBgImageChangesHelpers.update
|
|
632
|
+
export const deletePendingBgImageChange = _pendingBgImageChangesHelpers.delete
|
|
633
|
+
export const clearPendingBgImageChanges = _pendingBgImageChangesHelpers.clear
|
|
634
|
+
export const getPendingBgImageChange = _pendingBgImageChangesHelpers.get
|
|
635
|
+
|
|
618
636
|
// SEO changes mutations - using helpers
|
|
619
637
|
export const setPendingSeoChange = _pendingSeoChangesHelpers.set
|
|
620
638
|
export const updatePendingSeoChange = _pendingSeoChangesHelpers.update
|
|
@@ -1332,6 +1350,7 @@ export function resetAllState(): void {
|
|
|
1332
1350
|
pendingInserts.value = new Map()
|
|
1333
1351
|
pendingImageChanges.value = new Map()
|
|
1334
1352
|
pendingColorChanges.value = new Map()
|
|
1353
|
+
pendingBgImageChanges.value = new Map()
|
|
1335
1354
|
pendingSeoChanges.value = new Map()
|
|
1336
1355
|
pendingAttributeChanges.value = new Map()
|
|
1337
1356
|
manifest.value = { entries: {}, components: {}, componentDefinitions: {} }
|
package/src/editor/storage.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { STORAGE_KEYS } from './constants'
|
|
|
2
2
|
import type {
|
|
3
3
|
CmsSettings,
|
|
4
4
|
PendingAttributeChange,
|
|
5
|
+
PendingBackgroundImageChange,
|
|
5
6
|
PendingChange,
|
|
6
7
|
PendingColorChange,
|
|
7
8
|
PendingImageChange,
|
|
8
9
|
SavedAttributeEdits,
|
|
10
|
+
SavedBackgroundImageEdits,
|
|
9
11
|
SavedColorEdits,
|
|
10
12
|
SavedEdits,
|
|
11
13
|
SavedImageEdits,
|
|
@@ -180,6 +182,53 @@ export function clearAttributeEditsFromStorage(): void {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Background Image Edits
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
export function saveBgImageEditsToStorage(pendingBgImageChanges: Map<string, PendingBackgroundImageChange>): void {
|
|
190
|
+
const edits: SavedBackgroundImageEdits = {}
|
|
191
|
+
|
|
192
|
+
pendingBgImageChanges.forEach((change, cmsId) => {
|
|
193
|
+
if (change.isDirty) {
|
|
194
|
+
edits[cmsId] = {
|
|
195
|
+
originalBgImageClass: change.originalBgImageClass,
|
|
196
|
+
newBgImageClass: change.newBgImageClass,
|
|
197
|
+
originalBgSize: change.originalBgSize,
|
|
198
|
+
newBgSize: change.newBgSize,
|
|
199
|
+
originalBgPosition: change.originalBgPosition,
|
|
200
|
+
newBgPosition: change.newBgPosition,
|
|
201
|
+
originalBgRepeat: change.originalBgRepeat,
|
|
202
|
+
newBgRepeat: change.newBgRepeat,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
sessionStorage.setItem(STORAGE_KEYS.PENDING_BG_IMAGE_EDITS, JSON.stringify(edits))
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.warn('[CMS] Failed to save bg image edits to storage:', e)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function loadBgImageEditsFromStorage(): SavedBackgroundImageEdits {
|
|
215
|
+
try {
|
|
216
|
+
const stored = sessionStorage.getItem(STORAGE_KEYS.PENDING_BG_IMAGE_EDITS)
|
|
217
|
+
return stored ? JSON.parse(stored) : {}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.warn('[CMS] Failed to load bg image edits from storage:', e)
|
|
220
|
+
return {}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function clearBgImageEditsFromStorage(): void {
|
|
225
|
+
try {
|
|
226
|
+
sessionStorage.removeItem(STORAGE_KEYS.PENDING_BG_IMAGE_EDITS)
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.warn('[CMS] Failed to clear bg image edits from storage:', e)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
183
232
|
// ============================================================================
|
|
184
233
|
// Settings
|
|
185
234
|
// ============================================================================
|
|
@@ -287,4 +336,5 @@ export function clearAllEditsFromStorage(): void {
|
|
|
287
336
|
clearImageEditsFromStorage()
|
|
288
337
|
clearColorEditsFromStorage()
|
|
289
338
|
clearAttributeEditsFromStorage()
|
|
339
|
+
clearBgImageEditsFromStorage()
|
|
290
340
|
}
|
package/src/editor/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type {
|
|
|
5
5
|
Attribute,
|
|
6
6
|
AvailableColors,
|
|
7
7
|
AvailableTextStyles,
|
|
8
|
+
BackgroundImageMetadata,
|
|
8
9
|
CanonicalUrl,
|
|
9
10
|
CmsManifest,
|
|
10
11
|
CollectionDefinition,
|
|
@@ -94,6 +95,28 @@ export interface PendingColorChange {
|
|
|
94
95
|
isDirty: boolean
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
export interface PendingBackgroundImageChange {
|
|
99
|
+
element: HTMLElement
|
|
100
|
+
cmsId: string
|
|
101
|
+
/** Original bg-[url()] class */
|
|
102
|
+
originalBgImageClass: string
|
|
103
|
+
/** Current bg-[url()] class */
|
|
104
|
+
newBgImageClass: string
|
|
105
|
+
/** Original bg-size class (or empty) */
|
|
106
|
+
originalBgSize: string
|
|
107
|
+
/** Current bg-size class */
|
|
108
|
+
newBgSize: string
|
|
109
|
+
/** Original bg-position class (or empty) */
|
|
110
|
+
originalBgPosition: string
|
|
111
|
+
/** Current bg-position class */
|
|
112
|
+
newBgPosition: string
|
|
113
|
+
/** Original bg-repeat class (or empty) */
|
|
114
|
+
originalBgRepeat: string
|
|
115
|
+
/** Current bg-repeat class */
|
|
116
|
+
newBgRepeat: string
|
|
117
|
+
isDirty: boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
export interface ColorEditorState {
|
|
98
121
|
isOpen: boolean
|
|
99
122
|
targetElementId: string | null
|
|
@@ -132,6 +155,21 @@ export interface SavedColorEdits {
|
|
|
132
155
|
[cmsId: string]: SavedColorEdit
|
|
133
156
|
}
|
|
134
157
|
|
|
158
|
+
export interface SavedBackgroundImageEdit {
|
|
159
|
+
originalBgImageClass: string
|
|
160
|
+
newBgImageClass: string
|
|
161
|
+
originalBgSize: string
|
|
162
|
+
newBgSize: string
|
|
163
|
+
originalBgPosition: string
|
|
164
|
+
newBgPosition: string
|
|
165
|
+
originalBgRepeat: string
|
|
166
|
+
newBgRepeat: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface SavedBackgroundImageEdits {
|
|
170
|
+
[cmsId: string]: SavedBackgroundImageEdit
|
|
171
|
+
}
|
|
172
|
+
|
|
135
173
|
/** Color change details for updating element color classes */
|
|
136
174
|
export interface ColorChangePayload {
|
|
137
175
|
/** The color class to replace (e.g., 'bg-blue-500') */
|
|
@@ -139,7 +177,20 @@ export interface ColorChangePayload {
|
|
|
139
177
|
/** The new color class (e.g., 'bg-red-500') */
|
|
140
178
|
newClass: string
|
|
141
179
|
/** Type of color/style change */
|
|
142
|
-
type:
|
|
180
|
+
type:
|
|
181
|
+
| 'bg'
|
|
182
|
+
| 'text'
|
|
183
|
+
| 'border'
|
|
184
|
+
| 'hoverBg'
|
|
185
|
+
| 'hoverText'
|
|
186
|
+
| 'fontWeight'
|
|
187
|
+
| 'fontStyle'
|
|
188
|
+
| 'textDecoration'
|
|
189
|
+
| 'fontSize'
|
|
190
|
+
| 'bgImage'
|
|
191
|
+
| 'bgSize'
|
|
192
|
+
| 'bgPosition'
|
|
193
|
+
| 'bgRepeat'
|
|
143
194
|
/** Path to the source file where the color class is defined */
|
|
144
195
|
sourcePath?: string
|
|
145
196
|
/** Line number where the color class is defined */
|