chameleon-backgrounds 3.0.0 → 3.0.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.
@@ -1,348 +1,339 @@
1
1
  (function webpackUniversalModuleDefinition(root, factory) {
2
- if (typeof exports === 'object' && typeof module === 'object')
3
- module.exports = factory();
4
- else if (typeof define === 'function' && define.amd)
5
- define([], factory);
6
- else if (typeof exports === 'object')
7
- exports["ChameleonBackgrounds"] = factory();
8
- else
9
- root["ChameleonBackgrounds"] = factory();
2
+ if(typeof exports === 'object' && typeof module === 'object')
3
+ module.exports = factory();
4
+ else if(typeof define === 'function' && define.amd)
5
+ define([], factory);
6
+ else if(typeof exports === 'object')
7
+ exports["ChameleonBackgrounds"] = factory();
8
+ else
9
+ root["ChameleonBackgrounds"] = factory();
10
10
  })(this, () => {
11
- return /******/ (() => { // webpackBootstrap
11
+ return /******/ (() => { // webpackBootstrap
12
12
  /******/ "use strict";
13
13
  /******/ // The require scope
14
14
  /******/ const __webpack_require__ = {};
15
- /******/
15
+ /******/
16
16
  /************************************************************************/
17
17
  /******/ /* webpack/runtime/define property getters */
18
18
  /******/ (() => {
19
19
  /******/ // define getter/value functions for harmony exports
20
20
  /******/ __webpack_require__.d = (exports, definition) => {
21
- /******/ if (Array.isArray(definition)) {
21
+ /******/ if(Array.isArray(definition)) {
22
22
  /******/ var i = 0;
23
- /******/ while (i < definition.length) {
23
+ /******/ while(i < definition.length) {
24
24
  /******/ var key = definition[i++];
25
25
  /******/ var binding = definition[i++];
26
- /******/ if (!__webpack_require__.o(exports, key)) {
27
- /******/ if (binding === 0) {
26
+ /******/ if(!__webpack_require__.o(exports, key)) {
27
+ /******/ if(binding === 0) {
28
28
  /******/ Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });
29
- /******/
30
- } else {
29
+ /******/ } else {
31
30
  /******/ Object.defineProperty(exports, key, { enumerable: true, get: binding });
32
- /******/
33
- }
34
- /******/
35
- } else if (binding === 0) { i++; }
36
- /******/
37
- }
38
- /******/
39
- } else {
40
- /******/ for (var key in definition) {
41
- /******/ if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
31
+ /******/ }
32
+ /******/ } else if(binding === 0) { i++; }
33
+ /******/ }
34
+ /******/ } else {
35
+ /******/ for(var key in definition) {
36
+ /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
42
37
  /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
43
- /******/
44
- }
45
- /******/
46
- }
47
- /******/
48
- }
49
- /******/
50
- };
51
- /******/
52
- })();
53
- /******/
38
+ /******/ }
39
+ /******/ }
40
+ /******/ }
41
+ /******/ };
42
+ /******/ })();
43
+ /******/
54
44
  /******/ /* webpack/runtime/hasOwnProperty shorthand */
55
45
  /******/ (() => {
56
46
  /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
57
- /******/
58
- })();
59
- /******/
47
+ /******/ })();
48
+ /******/
60
49
  /******/ /* webpack/runtime/make namespace object */
61
50
  /******/ (() => {
62
51
  /******/ // define __esModule on exports
63
52
  /******/ __webpack_require__.r = (exports) => {
64
- /******/ if (Symbol.toStringTag) {
53
+ /******/ if(Symbol.toStringTag) {
65
54
  /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
66
- /******/
67
- }
55
+ /******/ }
68
56
  /******/ Object.defineProperty(exports, '__esModule', { value: true });
69
- /******/
70
- };
71
- /******/
72
- })();
73
- /******/
74
- /************************************************************************/
75
- let __webpack_exports__ = {};
76
- /*!**************************************!*\
77
- !*** ./src/chameleon-backgrounds.js ***!
78
- \**************************************/
79
- __webpack_require__.r(__webpack_exports__);
57
+ /******/ };
58
+ /******/ })();
59
+ /******/
60
+ /************************************************************************/
61
+ let __webpack_exports__ = {};
62
+ /*!**************************************!*\
63
+ !*** ./src/chameleon-backgrounds.js ***!
64
+ \**************************************/
65
+ __webpack_require__.r(__webpack_exports__);
80
66
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
81
67
  /* harmony export */ ChameleonBackgrounds: () => (/* binding */ ChameleonBackgrounds),
82
68
  /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
83
- /* harmony export */
84
- });
85
- /**
86
- * _____ _ _
87
- * / ____| | | |
88
- * | | | |__ __ _ _ __ ___ ___| | ___ ___ _ __
89
- * | | | '_ \ / _` | '_ ` _ \ / _ \ |/ _ \/ _ \| '_ \
90
- * | |____| | | | (_| | | | | | | __/ | __/ (_) | | | |
91
- * \_____|_| |_|\__,_|_| |_| |_|\___|_|\___|\___|_| |_|_
92
- * | _ \ | | | |
93
- * | |_) | __ _ ___| | ____ _ _ __ ___ _ _ _ __ __| |___
94
- * | _ < / _` |/ __| |/ / _` | '__/ _ \| | | | '_ \ / _` / __|
95
- * | |_) | (_| | (__| < (_| | | | (_) | |_| | | | | (_| \__ \
96
- * |____/ \__,_|\___|_|\_\__, |_| \___/ \__,_|_| |_|\__,_|___/
97
- * __/ |
98
- * |___/
99
- *
100
- * @module ChameleonBackgrounds
101
- * @version 3.0.0
102
- * @author Lennart van Ballegoij (https://weblenn.com/)
103
- * @license MIT
104
- * @see https://github.com/WebLenn/ChameleonBackgrounds
105
- *
106
- * A zero-dependency JavaScript library to dynamically load background
107
- * images with elegant fade-in transitions and slideshow support.
108
- */
109
-
110
- /**
111
- * @typedef {Object} ChameleonImageConfig
112
- * @property {string} url - The fallback URL.
113
- * @property {string} [srcset] - Optional srcset.
114
- * @property {string} [sizes] - Optional sizes.
115
- *
116
- * @typedef {Object} ChameleonOptions
117
- * @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
118
- * @property {'single'|'slider'} type - Background mode.
119
- * @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).
120
- * @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
121
- * @property {string} [overlayImage] - Optional overlay pattern image URL.
122
- * @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
123
- * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.
124
- * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
125
- * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
126
- * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).
127
- * @property {boolean} [lazyLoad=true] - Whether to wait until element is in viewport using IntersectionObserver.
128
- * @property {'solid'|'crossfade'} [transitionMode='solid'] - Transition mode: 'solid' (fade to color) or 'crossfade' (fade between images).
129
- */
130
-
131
- class ChameleonBackgrounds {
132
- /** @type {ChameleonOptions} */
133
- static #DEFAULTS = Object.freeze({
134
- element: 'body',
135
- type: 'single',
136
- src: '',
137
- overlayColor: '#0f1e25',
138
- overlayImage: null,
139
- minOverlay: 0,
140
- transitionDuration: 2000,
141
- sliderDuration: 8000,
142
- sliderLoop: false,
143
- fetchPriority: 'auto',
144
- lazyLoad: true,
145
- transitionMode: 'solid',
146
- respectReducedMotion: false,
147
- });
148
-
149
- // Legacy snake_case → camelCase alias map
150
- static #ALIASES = Object.freeze({
151
- transition_duration: 'transitionDuration',
152
- slider_duration: 'sliderDuration',
153
- slider_loop: 'sliderLoop',
154
- min_overlay: 'minOverlay',
155
- overlay_color: 'overlayColor',
156
- overlay_image: 'overlayImage',
157
- fetch_priority: 'fetchPriority',
158
- lazy_load: 'lazyLoad',
159
- transition_mode: 'transitionMode',
160
- respect_reduced_motion: 'respectReducedMotion',
161
- });
162
-
163
- /** @type {boolean} */
164
- static #globalStylesInjected = false;
165
-
166
- /** @type {ChameleonOptions} */
167
- #options;
168
-
169
- /** @type {HTMLElement} */
170
- #element;
171
-
172
- /** @type {string} */
173
- #uid;
174
-
175
- /** @type {string} */
176
- #originalHTML;
177
-
178
- /** @type {number|null} */
179
- #sliderIntervalId = null;
180
-
181
- /** @type {number} */
182
- #currentSlideIndex = 0;
183
-
184
- /** @type {boolean} */
185
- #isPaused = false;
186
-
187
- /** @type {IntersectionObserver|null} */
188
- #observer = null;
69
+ /* harmony export */ });
70
+ /**
71
+ * _____ _ _
72
+ * / ____| | | |
73
+ * | | | |__ __ _ _ __ ___ ___| | ___ ___ _ __
74
+ * | | | '_ \ / _` | '_ ` _ \ / _ \ |/ _ \/ _ \| '_ \
75
+ * | |____| | | | (_| | | | | | | __/ | __/ (_) | | | |
76
+ * \_____|_| |_|\__,_|_| |_| |_|\___|_|\___|\___|_| |_|_
77
+ * | _ \ | | | |
78
+ * | |_) | __ _ ___| | ____ _ _ __ ___ _ _ _ __ __| |___
79
+ * | _ < / _` |/ __| |/ / _` | '__/ _ \| | | | '_ \ / _` / __|
80
+ * | |_) | (_| | (__| < (_| | | | (_) | |_| | | | | (_| \__ \
81
+ * |____/ \__,_|\___|_|\_\__, |_| \___/ \__,_|_| |_|\__,_|___/
82
+ * __/ |
83
+ * |___/
84
+ *
85
+ * @module ChameleonBackgrounds
86
+ * @version 3.0.1
87
+ * @author Lennart van Ballegoij (https://weblenn.com/)
88
+ * @license MIT
89
+ * @see https://github.com/WebLenn/ChameleonBackgrounds
90
+ *
91
+ * A zero-dependency JavaScript library to dynamically load background
92
+ * images with elegant fade-in transitions and slideshow support.
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} ChameleonImageConfig
97
+ * @property {string} url - The fallback URL.
98
+ * @property {string} [srcset] - Optional srcset.
99
+ * @property {string} [sizes] - Optional sizes.
100
+ *
101
+ * @typedef {Object} ChameleonOptions
102
+ * @property {string|HTMLElement} element - CSS selector or DOM element to attach to.
103
+ * @property {'single'|'slider'} type - Background mode.
104
+ * @property {string|string[]|ChameleonImageConfig|ChameleonImageConfig[]} src - Image URL(s) or config object(s).
105
+ * @property {string} overlayColor - Overlay color (hex, rgb, rgba, hsl).
106
+ * @property {string} [overlayImage] - Optional overlay pattern image URL.
107
+ * @property {number} [minOverlay=0] - Minimum overlay opacity after fade (0–1).
108
+ * @property {number} [transitionDuration=2000] - Fade duration in milliseconds.
109
+ * @property {number} [sliderDuration=8000] - Time each slide is shown in milliseconds.
110
+ * @property {boolean} [sliderLoop=false] - Whether the slider restarts after the last slide.
111
+ * @property {'high'|'low'|'auto'} [fetchPriority='auto'] - fetchPriority for the first loaded image (e.g., 'high' for LCP images).
112
+ * @property {boolean} [lazyLoad=true] - Whether to wait until element is in viewport using IntersectionObserver.
113
+ * @property {'solid'|'crossfade'} [transitionMode='solid'] - Transition mode: 'solid' (fade to color) or 'crossfade' (fade between images).
114
+ */
115
+
116
+ class ChameleonBackgrounds {
117
+ /** @type {ChameleonOptions} */
118
+ static #DEFAULTS = Object.freeze({
119
+ element: 'body',
120
+ type: 'single',
121
+ src: '',
122
+ overlayColor: '#0f1e25',
123
+ overlayImage: null,
124
+ minOverlay: 0,
125
+ transitionDuration: 2000,
126
+ sliderDuration: 8000,
127
+ sliderLoop: false,
128
+ fetchPriority: 'auto',
129
+ lazyLoad: true,
130
+ transitionMode: 'solid',
131
+ respectReducedMotion: false,
132
+ });
133
+
134
+ // Legacy snake_case → camelCase alias map
135
+ static #ALIASES = Object.freeze({
136
+ transition_duration: 'transitionDuration',
137
+ slider_duration: 'sliderDuration',
138
+ slider_loop: 'sliderLoop',
139
+ min_overlay: 'minOverlay',
140
+ overlay_color: 'overlayColor',
141
+ overlay_image: 'overlayImage',
142
+ fetch_priority: 'fetchPriority',
143
+ lazy_load: 'lazyLoad',
144
+ transition_mode: 'transitionMode',
145
+ respect_reduced_motion: 'respectReducedMotion',
146
+ });
147
+
148
+ /** @type {boolean} */
149
+ static #globalStylesInjected = false;
150
+
151
+ /** @type {ChameleonOptions} */
152
+ #options;
153
+
154
+ /** @type {HTMLElement} */
155
+ #element;
156
+
157
+ /** @type {string} */
158
+ #uid;
159
+
160
+ /** @type {string} */
161
+ #originalHTML;
162
+
163
+ /** @type {number|null} */
164
+ #sliderIntervalId = null;
165
+
166
+ /** @type {number} */
167
+ #currentSlideIndex = 0;
168
+
169
+ /** @type {boolean} */
170
+ #hasLoadedFirstBackground = false;
171
+
172
+ /** @type {number} */
173
+ #transitionId = 0;
174
+
175
+ /** @type {boolean} */
176
+ #isPaused = false;
177
+
178
+ /** @type {IntersectionObserver|null} */
179
+ #observer = null;
180
+
181
+ /** @type {boolean} */
182
+ #destroyed = false;
183
+
184
+ /**
185
+ * Create a new ChameleonBackgrounds instance.
186
+ * @param {Partial<ChameleonOptions>} [options]
187
+ */
188
+ constructor(options = {}) {
189
+ const normalized = ChameleonBackgrounds.#normalizeOptions(options);
190
+ this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };
191
+ this.#uid = ChameleonBackgrounds.#generateUID();
192
+ this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);
193
+ this.#originalHTML = this.#element.innerHTML;
194
+
195
+ this.#init();
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Public API
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Clean up all injected DOM, styles, and intervals.
204
+ * Restores the target element to its original state.
205
+ */
206
+ destroy() {
207
+ if (this.#destroyed) return;
208
+ this.#destroyed = true;
209
+
210
+ // Stop any running slider
211
+ if (this.#sliderIntervalId !== null) {
212
+ clearTimeout(this.#sliderIntervalId);
213
+ this.#sliderIntervalId = null;
214
+ }
189
215
 
190
- /** @type {boolean} */
191
- #destroyed = false;
216
+ if (this.#observer) {
217
+ this.#observer.disconnect();
218
+ this.#observer = null;
219
+ }
192
220
 
193
- /**
194
- * Create a new ChameleonBackgrounds instance.
195
- * @param {Partial<ChameleonOptions>} [options]
196
- */
197
- constructor(options = {}) {
198
- const normalized = ChameleonBackgrounds.#normalizeOptions(options);
199
- this.#options = { ...ChameleonBackgrounds.#DEFAULTS, ...normalized };
200
- this.#uid = ChameleonBackgrounds.#generateUID();
201
- this.#element = ChameleonBackgrounds.#resolveElement(this.#options.element);
202
- this.#originalHTML = this.#element.innerHTML;
221
+ // Clean up dynamic classes on host
222
+ this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');
203
223
 
204
- this.#init();
224
+ // Unwrap the original content instead of resetting innerHTML
225
+ const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
226
+ if (wrapper && wrapper.parentNode === this.#element) {
227
+ while (wrapper.firstChild) {
228
+ this.#element.insertBefore(wrapper.firstChild, wrapper);
205
229
  }
230
+ this.#element.removeChild(wrapper);
231
+ }
206
232
 
207
- // ---------------------------------------------------------------------------
208
- // Public API
209
- // ---------------------------------------------------------------------------
210
-
211
- /**
212
- * Clean up all injected DOM, styles, and intervals.
213
- * Restores the target element to its original state.
214
- */
215
- destroy() {
216
- if (this.#destroyed) return;
217
- this.#destroyed = true;
218
-
219
- // Stop any running slider
220
- if (this.#sliderIntervalId !== null) {
221
- clearInterval(this.#sliderIntervalId);
222
- this.#sliderIntervalId = null;
223
- }
224
-
225
- if (this.#observer) {
226
- this.#observer.disconnect();
227
- this.#observer = null;
228
- }
233
+ // Remove loader
234
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
235
+ if (loader) {
236
+ loader.remove();
237
+ }
229
238
 
230
- // Clean up dynamic classes on host
231
- this.#element.classList.remove(`cbg-host-${this.#uid}`, 'cbg-host');
239
+ // Remove temporary crossfade elements
240
+ const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);
241
+ crossfadeElements.forEach(el => el.remove());
242
+
243
+ // Remove inline CSS Variables and background-image
244
+ this.#element.style.backgroundImage = '';
245
+ this.#element.style.removeProperty('--cbg-duration');
246
+ this.#element.style.removeProperty('--cbg-overlay-color');
247
+ this.#element.style.removeProperty('--cbg-overlay-image');
248
+ this.#element.style.removeProperty('--cbg-min-overlay');
249
+ }
250
+
251
+ /**
252
+ * The resolved options (read-only copy).
253
+ * @returns {ChameleonOptions}
254
+ */
255
+ get options() {
256
+ return { ...this.#options };
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // Initialization
261
+ // ---------------------------------------------------------------------------
262
+
263
+ #init() {
264
+ this.#injectGlobalStyles();
265
+ this.#updateCSSVariables();
266
+ this.#buildDOM();
267
+
268
+ // Check for prefers-reduced-motion to auto-pause slider if enabled
269
+ if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
270
+ this.#isPaused = true;
271
+ }
232
272
 
233
- // Unwrap the original content instead of resetting innerHTML
234
- const wrapper = this.#element.querySelector(`#cbg-inner-${this.#uid}`);
235
- if (wrapper && wrapper.parentNode === this.#element) {
236
- while (wrapper.firstChild) {
237
- this.#element.insertBefore(wrapper.firstChild, wrapper);
273
+ if (this.#options.lazyLoad && 'IntersectionObserver' in window) {
274
+ this.#observer = new IntersectionObserver((entries) => {
275
+ entries.forEach((entry) => {
276
+ if (entry.isIntersecting) {
277
+ this.#observer.disconnect();
278
+ this.#observer = null;
279
+ this.#retrieveBackground();
238
280
  }
239
- this.#element.removeChild(wrapper);
240
- }
241
-
242
- // Remove loader
243
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
244
- if (loader) {
245
- loader.remove();
246
- }
247
-
248
- // Remove temporary crossfade elements
249
- const crossfadeElements = this.#element.querySelectorAll(`.cbg-crossfade-${this.#uid}`);
250
- crossfadeElements.forEach(el => el.remove());
251
-
252
- // Remove inline CSS Variables and background-image
253
- this.#element.style.backgroundImage = '';
254
- this.#element.style.removeProperty('--cbg-duration');
255
- this.#element.style.removeProperty('--cbg-overlay-color');
256
- this.#element.style.removeProperty('--cbg-overlay-image');
257
- this.#element.style.removeProperty('--cbg-min-overlay');
258
- }
259
-
260
- /**
261
- * The resolved options (read-only copy).
262
- * @returns {ChameleonOptions}
263
- */
264
- get options() {
265
- return { ...this.#options };
266
- }
267
-
268
- // ---------------------------------------------------------------------------
269
- // Initialization
270
- // ---------------------------------------------------------------------------
271
-
272
- #init() {
273
- this.#injectGlobalStyles();
274
- this.#updateCSSVariables();
275
- this.#buildDOM();
276
-
277
- // Check for prefers-reduced-motion to auto-pause slider if enabled
278
- if (this.#options.respectReducedMotion && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
279
- this.#isPaused = true;
280
- }
281
-
282
- if (this.#options.lazyLoad && 'IntersectionObserver' in window) {
283
- this.#observer = new IntersectionObserver((entries) => {
284
- entries.forEach((entry) => {
285
- if (entry.isIntersecting) {
286
- this.#observer.disconnect();
287
- this.#observer = null;
288
- this.#retrieveBackground();
289
- }
290
- });
291
- });
292
- this.#observer.observe(this.#element);
281
+ });
282
+ });
283
+ this.#observer.observe(this.#element);
284
+ } else {
285
+ if (this.#element.matches('body')) {
286
+ queueMicrotask(() => this.#retrieveBackground());
287
+ } else {
288
+ if (document.readyState === 'complete') {
289
+ this.#retrieveBackground();
293
290
  } else {
294
- if (this.#element.matches('body')) {
295
- queueMicrotask(() => this.#retrieveBackground());
296
- } else {
297
- if (document.readyState === 'complete') {
298
- this.#retrieveBackground();
299
- } else {
300
- window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
301
- }
302
- }
303
- }
304
- }
305
-
306
- // ---------------------------------------------------------------------------
307
- // Play / Pause API
308
- // ---------------------------------------------------------------------------
309
-
310
- /**
311
- * Pause the slideshow.
312
- */
313
- pause() {
314
- if (this.#destroyed || this.#options.type !== 'slider') return;
315
- this.#isPaused = true;
316
- if (this.#sliderIntervalId !== null) {
317
- clearInterval(this.#sliderIntervalId);
318
- this.#sliderIntervalId = null;
319
- }
320
- }
321
-
322
- /**
323
- * Resume the slideshow.
324
- */
325
- play() {
326
- if (this.#destroyed || this.#options.type !== 'slider') return;
327
- this.#isPaused = false;
328
- // Only restart if not currently running
329
- if (this.#sliderIntervalId === null) {
330
- this.#startSliderLoop();
291
+ window.addEventListener('load', () => this.#retrieveBackground(), { once: true });
331
292
  }
332
293
  }
294
+ }
295
+ }
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Play / Pause API
299
+ // ---------------------------------------------------------------------------
300
+
301
+ /**
302
+ * Pause the slideshow.
303
+ */
304
+ pause() {
305
+ if (this.#destroyed || this.#options.type !== 'slider') return;
306
+ this.#isPaused = true;
307
+ if (this.#sliderIntervalId !== null) {
308
+ clearTimeout(this.#sliderIntervalId);
309
+ this.#sliderIntervalId = null;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Resume the slideshow.
315
+ */
316
+ play() {
317
+ if (this.#destroyed || this.#options.type !== 'slider') return;
318
+ this.#isPaused = false;
319
+ // Only restart if not currently running
320
+ if (this.#sliderIntervalId === null) {
321
+ this.#startSliderLoop();
322
+ }
323
+ }
333
324
 
334
- // ---------------------------------------------------------------------------
335
- // DOM Construction
336
- // ---------------------------------------------------------------------------
325
+ // ---------------------------------------------------------------------------
326
+ // DOM Construction
327
+ // ---------------------------------------------------------------------------
337
328
 
338
- /**
339
- * Inject global styles once.
340
- */
341
- #injectGlobalStyles() {
342
- if (ChameleonBackgrounds.#globalStylesInjected) return;
343
- ChameleonBackgrounds.#globalStylesInjected = true;
329
+ /**
330
+ * Inject global styles once.
331
+ */
332
+ #injectGlobalStyles() {
333
+ if (ChameleonBackgrounds.#globalStylesInjected) return;
334
+ ChameleonBackgrounds.#globalStylesInjected = true;
344
335
 
345
- const css = `
336
+ const css = `
346
337
  .cbg-host {
347
338
  position: relative;
348
339
  }
@@ -388,356 +379,364 @@
388
379
  }
389
380
  `;
390
381
 
391
- const style = document.createElement('style');
392
- style.id = 'chameleon-global-styles';
393
- style.textContent = css;
394
- document.head.appendChild(style);
395
- }
396
-
397
- /**
398
- * Apply CSS custom properties to the host element.
399
- */
400
- #updateCSSVariables() {
401
- this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');
402
- this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);
403
- this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);
404
-
405
- if (this.#options.overlayImage) {
406
- this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);
407
- } else {
408
- this.#element.style.setProperty('--cbg-overlay-image', 'none');
409
- }
410
-
411
- this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
412
- }
382
+ const style = document.createElement('style');
383
+ style.id = 'chameleon-global-styles';
384
+ style.textContent = css;
385
+ document.head.appendChild(style);
386
+ }
387
+
388
+ /**
389
+ * Apply CSS custom properties to the host element.
390
+ */
391
+ #updateCSSVariables() {
392
+ this.#element.classList.add(`cbg-host-${this.#uid}`, 'cbg-host');
393
+ this.#element.style.setProperty('--cbg-duration', `${this.#options.transitionDuration / 1000}s`);
394
+ this.#element.style.setProperty('--cbg-overlay-color', this.#options.overlayColor);
395
+
396
+ if (this.#options.overlayImage) {
397
+ this.#element.style.setProperty('--cbg-overlay-image', `url(${this.#options.overlayImage})`);
398
+ } else {
399
+ this.#element.style.setProperty('--cbg-overlay-image', 'none');
400
+ }
413
401
 
414
- /**
415
- * Wrap existing content and insert the overlay loader.
416
- */
417
- #buildDOM() {
418
- const uid = this.#uid;
402
+ this.#element.style.setProperty('--cbg-min-overlay', String(this.#options.minOverlay));
403
+ }
419
404
 
420
- const wrapper = document.createElement('div');
421
- wrapper.id = `cbg-inner-${uid}`;
422
- wrapper.classList.add('cbg-inner');
405
+ /**
406
+ * Wrap existing content and insert the overlay loader.
407
+ */
408
+ #buildDOM() {
409
+ const uid = this.#uid;
423
410
 
424
- // Move all child nodes from the element into the wrapper
425
- while (this.#element.firstChild) {
426
- wrapper.appendChild(this.#element.firstChild);
427
- }
411
+ const wrapper = document.createElement('div');
412
+ wrapper.id = `cbg-inner-${uid}`;
413
+ wrapper.classList.add('cbg-inner');
428
414
 
429
- const loader = document.createElement('div');
430
- loader.classList.add('cbg-loader', `cbg-loader-${uid}`);
415
+ // Move all child nodes from the element into the wrapper
416
+ while (this.#element.firstChild) {
417
+ wrapper.appendChild(this.#element.firstChild);
418
+ }
431
419
 
432
- // Append wrapper and loader
433
- this.#element.appendChild(wrapper);
434
- this.#element.appendChild(loader);
420
+ const loader = document.createElement('div');
421
+ loader.classList.add('cbg-loader', `cbg-loader-${uid}`);
422
+
423
+ // Append wrapper and loader
424
+ this.#element.appendChild(wrapper);
425
+ this.#element.appendChild(loader);
426
+ }
427
+
428
+ // ---------------------------------------------------------------------------
429
+ // Background Loading
430
+ // ---------------------------------------------------------------------------
431
+
432
+ /**
433
+ * Route to the correct loader based on type.
434
+ */
435
+ #retrieveBackground() {
436
+ if (this.#destroyed) return;
437
+
438
+ if (this.#options.type === 'single') {
439
+ const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
440
+ this.#loadBackground(singleSrc);
441
+ } else if (this.#options.type === 'slider') {
442
+ this.#startSlider();
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Preload an image and apply it as the element's background.
448
+ * Handles both string URLs and responsive image objects ({ url, srcset, sizes }).
449
+ * @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.
450
+ * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
451
+ * @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.
452
+ * @returns {Promise<void>}
453
+ */
454
+ #loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
455
+ if (this.#destroyed) return Promise.resolve();
456
+
457
+ return new Promise((resolve) => {
458
+ const img = new Image();
459
+
460
+ let urlStr = srcConfig;
461
+ if (typeof srcConfig === 'object' && srcConfig.url) {
462
+ urlStr = srcConfig.url;
463
+ if (srcConfig.srcset) img.srcset = srcConfig.srcset;
464
+ if (srcConfig.sizes) img.sizes = srcConfig.sizes;
435
465
  }
436
466
 
437
- // ---------------------------------------------------------------------------
438
- // Background Loading
439
- // ---------------------------------------------------------------------------
440
-
441
- /**
442
- * Route to the correct loader based on type.
443
- */
444
- #retrieveBackground() {
445
- if (this.#destroyed) return;
446
-
447
- if (this.#options.type === 'single') {
448
- const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
449
- this.#loadBackground(singleSrc);
450
- } else if (this.#options.type === 'slider') {
451
- this.#startSlider();
452
- }
467
+ if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
468
+ img.fetchPriority = this.#options.fetchPriority;
453
469
  }
454
470
 
455
- /**
456
- * Preload an image and apply it as the element's background.
457
- * Handles both string URLs and responsive image objects ({ url, srcset, sizes }).
458
- * @param {string|ChameleonImageConfig} srcConfig - Image configuration or URL.
459
- * @param {boolean} [isFirst=true] - Whether this is the first image (skips overlay reset).
460
- * @param {boolean} [isCrossfade=false] - If true, handles true crossfading without solid color drop.
461
- * @returns {Promise<void>}
462
- */
463
- #loadBackground(srcConfig, isFirst = true, isCrossfade = false) {
464
- if (this.#destroyed) return Promise.resolve();
465
-
466
- return new Promise((resolve) => {
467
- const img = new Image();
468
-
469
- let urlStr = srcConfig;
470
- if (typeof srcConfig === 'object' && srcConfig.url) {
471
- urlStr = srcConfig.url;
472
- if (srcConfig.srcset) img.srcset = srcConfig.srcset;
473
- if (srcConfig.sizes) img.sizes = srcConfig.sizes;
474
- }
471
+ // Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers
472
+ img.style.position = 'absolute';
473
+ img.style.visibility = 'hidden';
474
+ img.style.width = '0';
475
+ img.style.height = '0';
476
+ this.#element.appendChild(img);
475
477
 
476
- if (isFirst && 'fetchPriority' in img && this.#options.fetchPriority !== 'auto') {
477
- img.fetchPriority = this.#options.fetchPriority;
478
- }
478
+ img.onload = () => {
479
+ img.remove();
480
+ if (this.#destroyed) return resolve();
479
481
 
480
- // Append to DOM to ensure viewport-based `sizes` calculate correctly in all browsers
481
- img.style.position = 'absolute';
482
- img.style.visibility = 'hidden';
483
- img.style.width = '0';
484
- img.style.height = '0';
485
- this.#element.appendChild(img);
482
+ // `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`
483
+ const finalUrl = img.currentSrc || urlStr;
486
484
 
487
- img.onload = () => {
488
- img.remove();
489
- if (this.#destroyed) return resolve();
485
+ if (isCrossfade && !isFirst) {
486
+ // --- Crossfade Transition Mode ---
487
+ const crossfadeDiv = document.createElement('div');
488
+ crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);
489
+ crossfadeDiv.style.backgroundImage = `url("${finalUrl}")`;
490
490
 
491
- // `img.currentSrc` retrieves the actual srcset chosen file, otherwise fallback to `urlStr`
492
- const finalUrl = img.currentSrc || urlStr;
493
-
494
- if (isCrossfade && !isFirst) {
495
- // --- Crossfade Transition Mode ---
496
- const crossfadeDiv = document.createElement('div');
497
- crossfadeDiv.classList.add('cbg-crossfade-el', `cbg-crossfade-${this.#uid}`);
498
- crossfadeDiv.style.backgroundImage = `url("${finalUrl}")`;
499
-
500
- // Insert right behind the loader (or directly at start of host)
501
- this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
502
-
503
- // Trigger layout so the transition works
504
- void crossfadeDiv.offsetWidth;
505
-
506
- crossfadeDiv.style.opacity = '1';
507
-
508
- setTimeout(() => {
509
- if (this.#destroyed) return resolve();
510
- this.#element.style.backgroundImage = `url("${finalUrl}")`;
511
- this.#element.style.backgroundSize = 'cover';
512
- this.#element.style.backgroundRepeat = 'no-repeat';
513
- crossfadeDiv.remove();
514
- resolve();
515
- }, this.#options.transitionDuration);
516
-
517
- } else {
518
- // --- Solid Transition Mode or First Load ---
519
- this.#element.style.backgroundImage = `url("${finalUrl}")`;
520
- this.#element.style.backgroundSize = 'cover';
521
- this.#element.style.backgroundRepeat = 'no-repeat';
522
-
523
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
524
- if (loader) {
525
- loader.style.opacity = String(this.#options.minOverlay);
526
- }
527
- resolve();
528
- }
529
- };
530
-
531
- img.onerror = () => {
532
- img.remove();
533
- console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
534
- resolve(); // resolve anyway to keep logic flowing
535
- };
536
-
537
- img.src = urlStr;
538
- });
539
- }
540
-
541
- /**
542
- /**
543
- * Reload background with a fade-out → swap → fade-in cycle.
544
- * @param {string} [src] - Optional new image URL. If omitted, reloads using current options.
545
- * @returns {Promise<void>}
546
- */
547
- reloadBackground(src) {
548
- if (this.#destroyed) return Promise.resolve();
549
-
550
- if (src !== undefined) {
551
- if (this.#options.type === 'slider' && typeof src === 'string') {
552
- // If the user passes a comma-separated string, split it.
553
- this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
554
- } else {
555
- this.#options.src = src;
556
- }
557
- }
491
+ // Insert right behind the loader (or directly at start of host)
492
+ this.#element.insertBefore(crossfadeDiv, this.#element.firstChild);
558
493
 
559
- // Stop any running slider
560
- if (this.#sliderIntervalId !== null) {
561
- clearInterval(this.#sliderIntervalId);
562
- this.#sliderIntervalId = null;
563
- }
494
+ // Trigger layout so the transition works
495
+ void crossfadeDiv.offsetWidth;
564
496
 
565
- // For solid mode, we fade the loader up. For crossfade, we don't.
566
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
567
- if (loader && this.#options.transitionMode === 'solid') {
568
- loader.style.opacity = '1';
569
- }
497
+ crossfadeDiv.style.opacity = '1';
570
498
 
571
- return new Promise((resolve) => {
572
- const waitTime = this.#options.transitionMode === 'solid' ? this.#options.transitionDuration : 0;
573
499
  setTimeout(() => {
574
500
  if (this.#destroyed) return resolve();
501
+ this.#element.style.backgroundImage = `url("${finalUrl}")`;
502
+ this.#element.style.backgroundSize = 'cover';
503
+ this.#element.style.backgroundRepeat = 'no-repeat';
504
+ crossfadeDiv.remove();
505
+ resolve();
506
+ }, this.#options.transitionDuration);
575
507
 
576
- if (this.#options.type === 'single') {
577
- const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
578
- this.#loadBackground(singleSrc, false, this.#options.transitionMode === 'crossfade').then(resolve);
579
- } else if (this.#options.type === 'slider') {
580
- this.#startSlider();
581
- resolve();
582
- }
583
- }, waitTime);
584
- });
585
- }
508
+ } else {
509
+ // --- Solid Transition Mode or First Load ---
510
+ this.#element.style.backgroundImage = `url("${finalUrl}")`;
511
+ this.#element.style.backgroundSize = 'cover';
512
+ this.#element.style.backgroundRepeat = 'no-repeat';
586
513
 
587
- /**
588
- * Update options on the fly without destroying the instance.
589
- * @param {Object} newOptions - The new options to merge.
590
- */
591
- reloadOptions(newOptions) {
592
- if (this.#destroyed) return;
514
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
515
+ if (loader) {
516
+ loader.style.opacity = String(this.#options.minOverlay);
517
+ }
518
+ this.#hasLoadedFirstBackground = true;
519
+ setTimeout(() => {
520
+ resolve();
521
+ }, this.#options.transitionDuration);
522
+ }
523
+ };
593
524
 
594
- const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
595
- this.#options = { ...this.#options, ...normalized };
525
+ img.onerror = () => {
526
+ img.remove();
527
+ console.warn(`[ChameleonBackgrounds] Failed to load image:`, srcConfig);
528
+ resolve(); // resolve anyway to keep logic flowing
529
+ };
596
530
 
597
- this.#updateCSSVariables();
598
- // Restart slider if needed
599
- if (this.#options.type === 'slider' && !this.#isPaused && !this.#sliderIntervalId) {
600
- this.#startSlider();
601
- }
531
+ img.src = urlStr;
532
+ });
533
+ }
534
+
535
+ /**
536
+ /**
537
+ * Reload background with a fade-out → swap → fade-in cycle.
538
+ * @param {string} [src] - Optional new image URL. If omitted, reloads using current options.
539
+ * @returns {Promise<void>}
540
+ */
541
+ reloadBackground(src) {
542
+ if (this.#destroyed) return Promise.resolve();
543
+
544
+ this.#transitionId++;
545
+
546
+ if (src !== undefined) {
547
+ if (this.#options.type === 'slider' && typeof src === 'string') {
548
+ // If the user passes a comma-separated string, split it.
549
+ this.#options.src = src.includes(',') ? src.split(',').map(s => s.trim()) : [src];
550
+ } else {
551
+ this.#options.src = src;
602
552
  }
553
+ }
603
554
 
604
- // ---------------------------------------------------------------------------
605
- // Slider
606
- // ---------------------------------------------------------------------------
555
+ // Stop any running slider
556
+ if (this.#sliderIntervalId !== null) {
557
+ clearTimeout(this.#sliderIntervalId);
558
+ this.#sliderIntervalId = null;
559
+ }
607
560
 
608
- /**
609
- * Start the background slideshow.
610
- */
611
- #startSlider() {
612
- if (this.#destroyed) return;
561
+ if (this.#options.type === 'single') {
562
+ const singleSrc = Array.isArray(this.#options.src) ? this.#options.src[0] : this.#options.src;
563
+ return this.#cycleSliderSlide(singleSrc, this.#transitionId);
564
+ } else if (this.#options.type === 'slider') {
565
+ this.#startSlider(this.#transitionId);
566
+ return Promise.resolve();
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Update options on the fly without destroying the instance.
572
+ * @param {Object} newOptions - The new options to merge.
573
+ */
574
+ reloadOptions(newOptions) {
575
+ if (this.#destroyed) return;
576
+
577
+ const normalized = ChameleonBackgrounds.#normalizeOptions(newOptions);
578
+ this.#options = { ...this.#options, ...normalized };
579
+
580
+ this.#updateCSSVariables();
581
+ // In v1/v2, reloadOptions did not restart the slider; the user called reloadBackground.
582
+ // We intentionally do not auto-restart here to prevent concurrent race conditions.
583
+ }
584
+
585
+ // ---------------------------------------------------------------------------
586
+ // Slider
587
+ // ---------------------------------------------------------------------------
588
+
589
+ /**
590
+ * Start the background slideshow.
591
+ * @param {number} tid - The transition ID
592
+ */
593
+ #startSlider(tid = this.#transitionId) {
594
+ if (this.#destroyed) return;
595
+
596
+ const sources = this.#options.src;
597
+ if (!Array.isArray(sources) || sources.length === 0) {
598
+ console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
599
+ return;
600
+ }
613
601
 
614
- const sources = this.#options.src;
615
- if (!Array.isArray(sources) || sources.length === 0) {
616
- console.warn('[ChameleonBackgrounds] Slider mode requires an array of image URLs in `src`.');
617
- return;
618
- }
602
+ this.#currentSlideIndex = 0;
619
603
 
620
- // Load the first slide immediately
621
- this.#currentSlideIndex = 0;
622
- this.#loadBackground(sources[this.#currentSlideIndex]).then(() => {
623
- if (this.#destroyed) return;
604
+ if (this.#hasLoadedFirstBackground) {
605
+ // Instance already has a background, crossfade smoothly
606
+ this.#cycleSliderSlide(sources[this.#currentSlideIndex], tid).then(() => {
607
+ if (this.#destroyed || this.#transitionId !== tid) return;
608
+ this.#currentSlideIndex = 1;
609
+ if (sources.length > 1) this.#startSliderLoop();
610
+ });
611
+ } else {
612
+ // First load, load immediately without solid color flash
613
+ this.#loadBackground(sources[this.#currentSlideIndex], true, this.#options.transitionMode === 'crossfade').then(() => {
614
+ if (this.#destroyed || this.#transitionId !== tid) return;
615
+ this.#hasLoadedFirstBackground = true;
616
+ this.#currentSlideIndex = 1;
617
+ if (sources.length > 1) this.#startSliderLoop();
618
+ });
619
+ }
620
+ }
624
621
 
625
- this.#currentSlideIndex = 1;
626
- if (sources.length === 1) return; // only one slide, nothing to rotate
622
+ #startSliderLoop() {
623
+ if (this.#destroyed || this.#isPaused) return;
627
624
 
628
- this.#startSliderLoop();
629
- });
625
+ // Parse as integers to prevent string concatenation bugs if users passed strings
626
+ const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
627
+
628
+ this.#sliderIntervalId = setTimeout(() => {
629
+ if (this.#destroyed || this.#isPaused) {
630
+ this.#sliderIntervalId = null;
631
+ return;
630
632
  }
631
633
 
632
- #startSliderLoop() {
633
- const sources = this.#options.src;
634
+ const tid = this.#transitionId;
635
+ this.#cycleSliderSlide(this.#options.src[this.#currentSlideIndex], tid).then(() => {
636
+ if (this.#destroyed || this.#isPaused || this.#transitionId !== tid) {
637
+ this.#sliderIntervalId = null;
638
+ return;
639
+ }
634
640
 
635
- // Parse as integers to prevent string concatenation bugs if users passed strings
636
- const sDuration = parseInt(this.#options.sliderDuration, 10) || 8000;
637
- const tDuration = parseInt(this.#options.transitionDuration, 10) || 2000;
638
- const interval = sDuration + tDuration * (this.#options.transitionMode === 'solid' ? 2 : 1);
641
+ this.#currentSlideIndex++;
639
642
 
640
- this.#sliderIntervalId = setInterval(() => {
641
- if (this.#destroyed || this.#isPaused) {
642
- clearInterval(this.#sliderIntervalId);
643
+ if (this.#currentSlideIndex >= this.#options.src.length) {
644
+ if (this.#options.sliderLoop) {
645
+ this.#currentSlideIndex = 0;
646
+ } else {
643
647
  this.#sliderIntervalId = null;
644
648
  return;
645
649
  }
646
-
647
- this.#cycleSliderSlide(sources[this.#currentSlideIndex]);
648
- this.#currentSlideIndex++;
649
-
650
- if (this.#currentSlideIndex >= sources.length) {
651
- if (this.#options.sliderLoop) {
652
- this.#currentSlideIndex = 0;
653
- } else {
654
- clearInterval(this.#sliderIntervalId);
655
- this.#sliderIntervalId = null;
656
- }
657
- }
658
- }, interval);
659
- }
660
-
661
- /**
662
- * Cycle to a specific slide without resetting the slider instance.
663
- * @param {string|ChameleonImageConfig} src - The image URL/config to load.
664
- */
665
- #cycleSliderSlide(src) {
666
- if (this.#destroyed) return;
667
-
668
- if (this.#options.transitionMode === 'solid') {
669
- const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
670
- if (loader) {
671
- loader.style.opacity = '1';
672
- }
673
-
674
- setTimeout(() => {
675
- if (this.#destroyed) return;
676
- this.#loadBackground(src, false, false);
677
- }, this.#options.transitionDuration);
678
- } else {
679
- // Crossfade mode triggers immediately
680
- this.#loadBackground(src, false, true);
681
650
  }
682
- }
683
651
 
684
- // ---------------------------------------------------------------------------
685
- // Static Helpers
686
- // ---------------------------------------------------------------------------
687
-
688
- /**
689
- * Convert legacy snake_case option keys to camelCase.
690
- * @param {Object} opts
691
- * @returns {Object}
692
- */
693
- static #normalizeOptions(opts) {
694
- const result = {};
695
- for (const [key, value] of Object.entries(opts)) {
696
- const alias = ChameleonBackgrounds.#ALIASES[key];
697
- result[alias ?? key] = value;
698
- }
699
- return result;
700
- }
701
-
702
- /**
703
- * Resolve a CSS selector string or HTMLElement to an HTMLElement.
704
- * @param {string|HTMLElement} el
705
- * @returns {HTMLElement}
706
- */
707
- static #resolveElement(el) {
708
- if (el instanceof HTMLElement) return el;
709
- if (typeof el === 'string') {
710
- if (el === 'body') return document.body;
711
- const found = document.querySelector(el);
712
- if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);
713
- return /** @type {HTMLElement} */ (found);
652
+ this.#startSliderLoop();
653
+ });
654
+ }, sDuration);
655
+ }
656
+
657
+ /**
658
+ * Cycle to a specific slide without resetting the slider instance.
659
+ * @param {string|ChameleonImageConfig} src - The image URL/config to load.
660
+ * @param {number} tid - The transition ID
661
+ * @returns {Promise<void>}
662
+ */
663
+ #cycleSliderSlide(src, tid = this.#transitionId) {
664
+ if (this.#destroyed) return Promise.resolve();
665
+
666
+ return new Promise(resolve => {
667
+ if (this.#options.transitionMode === 'solid') {
668
+ const loader = this.#element.querySelector(`.cbg-loader-${this.#uid}`);
669
+ if (loader) {
670
+ loader.style.opacity = '1';
714
671
  }
715
- throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);
716
- }
717
672
 
718
- /**
719
- * Generate a short unique ID for scoping styles.
720
- * @returns {string}
721
- */
722
- static #generateUID() {
723
- // Prefer crypto.randomUUID where available, otherwise fall back
724
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
725
- return crypto.randomUUID().slice(0, 8);
726
- }
727
- // Fallback: random alphanumeric string
728
- return Array.from({ length: 8 }, () =>
729
- 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
730
- ).join('');
673
+ setTimeout(() => {
674
+ if (this.#destroyed || this.#transitionId !== tid) return resolve();
675
+ this.#loadBackground(src, false, false).then(resolve);
676
+ }, this.#options.transitionDuration);
677
+ } else {
678
+ // Crossfade mode triggers immediately
679
+ this.#loadBackground(src, false, true).then(resolve);
731
680
  }
681
+ });
682
+ }
683
+
684
+ // ---------------------------------------------------------------------------
685
+ // Static Helpers
686
+ // ---------------------------------------------------------------------------
687
+
688
+ /**
689
+ * Convert legacy snake_case option keys to camelCase.
690
+ * @param {Object} opts
691
+ * @returns {Object}
692
+ */
693
+ static #normalizeOptions(opts) {
694
+ const result = {};
695
+ for (const [key, value] of Object.entries(opts)) {
696
+ const alias = ChameleonBackgrounds.#ALIASES[key];
697
+ result[alias ?? key] = value;
698
+ }
699
+ return result;
700
+ }
701
+
702
+ /**
703
+ * Resolve a CSS selector string or HTMLElement to an HTMLElement.
704
+ * @param {string|HTMLElement} el
705
+ * @returns {HTMLElement}
706
+ */
707
+ static #resolveElement(el) {
708
+ if (el instanceof HTMLElement) return el;
709
+ if (typeof el === 'string') {
710
+ if (el === 'body') return document.body;
711
+ const found = document.querySelector(el);
712
+ if (!found) throw new Error(`[ChameleonBackgrounds] Element not found: ${el}`);
713
+ return /** @type {HTMLElement} */ (found);
714
+ }
715
+ throw new TypeError(`[ChameleonBackgrounds] Invalid element: ${el}`);
716
+ }
717
+
718
+ /**
719
+ * Generate a short unique ID for scoping styles.
720
+ * @returns {string}
721
+ */
722
+ static #generateUID() {
723
+ // Prefer crypto.randomUUID where available, otherwise fall back
724
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
725
+ return crypto.randomUUID().slice(0, 8);
732
726
  }
727
+ // Fallback: random alphanumeric string
728
+ return Array.from({ length: 8 }, () =>
729
+ 'abcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 36))
730
+ ).join('');
731
+ }
732
+ }
733
733
 
734
734
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ChameleonBackgrounds);
735
735
 
736
736
 
737
- __webpack_exports__ = __webpack_exports__["default"];
737
+ __webpack_exports__ = __webpack_exports__["default"];
738
738
  /******/ return __webpack_exports__;
739
- /******/
740
- })()
741
- ;
739
+ /******/ })()
740
+ ;
742
741
  });
743
742
  //# sourceMappingURL=chameleon-backgrounds.js.map