@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.
@@ -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 && dirtyAttributeChanges.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
+ }
@@ -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={{
@@ -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 || hasDirtySeoChanges.value || hasDirtyAttributeChanges.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: {} }
@@ -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
  }
@@ -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: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText' | 'fontWeight' | 'fontStyle' | 'textDecoration' | 'fontSize'
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 */