PageSourceSearch

https://anniegsdairy.com/wp-content/plugins/divi-essential/assets/js/spotlight-card.js?ver=1780064763

js anniegsdairy.com collected 2026-10-05 08:40:19 UTC 28,575 bytes, 976 lines download raw bytes

1/**
2 * Spotlight Card - Vanilla JavaScript
3 * A mouse-following glow effect for cards
4 * by Mahedi amin
5 * @version 2.0.0
6 * @license MIT
7 */
8
9(function (global) {
10  "use strict";
11
12  // Autoplay animation paths - smooth mathematical functions
13  const autoplayPaths = {
14    circle: (t) => ({
15      x: 50 + 35 * Math.cos(t * Math.PI * 2),
16      y: 50 + 35 * Math.sin(t * Math.PI * 2),
17    }),
18    horizontal: (t) => ({
19      x: 50 + 45 * Math.sin(t * Math.PI * 2),
20      y: 50,
21    }),
22    vertical: (t) => ({
23      x: 50,
24      y: 50 + 45 * Math.sin(t * Math.PI * 2),
25    }),
26    diagonal: (t) => ({
27      x: 50 + 45 * Math.sin(t * Math.PI * 2),
28      y: 50 + 45 * Math.sin(t * Math.PI * 2),
29    }),
30    figure8: (t) => ({
31      x: 50 + 35 * Math.sin(t * Math.PI * 2),
32      y: 50 + 25 * Math.sin(t * Math.PI * 4),
33    }),
34  };
35
36  // Speed in milliseconds for one cycle
37  const autoplaySpeeds = {
38    slow: 6000,
39    normal: 3000,
40    fast: 1500,
41  };
42
43  /**
44   * Color conversion utilities
45   */
46  const ColorUtils = {
47    /**
48     * Parse any color format and return RGB values
49     * @param {string} color - hex, rgb, rgba, hsl, hsla, or named color
50     * @returns {object|null} - {r, g, b} or null if invalid
51     */
52    parseColor(color) {
53      if (!color || typeof color !== "string") return null;
54
55      color = color.trim().toLowerCase();
56
57      // Handle hex colors
58      if (color.startsWith("#")) {
59        return this.hexToRgb(color);
60      }
61
62      // Handle rgb/rgba
63      if (color.startsWith("rgb")) {
64        const match = color.match(
65          /rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i,
66        );
67        if (match) {
68          return {
69            r: parseInt(match[1], 10),
70            g: parseInt(match[2], 10),
71            b: parseInt(match[3], 10),
72          };
73        }
74      }
75
76      // Handle hsl/hsla - convert to RGB first
77      if (color.startsWith("hsl")) {
78        const match = color.match(
79          /hsla?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/i,
80        );
81        if (match) {
82          return this.hslToRgb(
83            parseFloat(match[1]),
84            parseFloat(match[2]),
85            parseFloat(match[3]),
86          );
87        }
88      }
89
90      // Handle named colors using a temporary element
91      if (typeof document !== "undefined") {
92        const temp = document.createElement("div");
93        temp.style.color = color;
94        document.body.appendChild(temp);
95        const computed = getComputedStyle(temp).color;
96        document.body.removeChild(temp);
97
98        const match = computed.match(
99          /rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i,
100        );
101        if (match) {
102          return {
103            r: parseInt(match[1], 10),
104            g: parseInt(match[2], 10),
105            b: parseInt(match[3], 10),
106          };
107        }
108      }
109
110      return null;
111    },
112
113    /**
114     * Convert hex color to RGB
115     * @param {string} hex - hex color (#fff, #ffffff, #ffffffff)
116     * @returns {object|null} - {r, g, b} or null
117     */
118    hexToRgb(hex) {
119      // Remove # if present
120      hex = hex.replace(/^#/, "");
121
122      // Handle shorthand (#fff -> #ffffff)
123      if (hex.length === 3 || hex.length === 4) {
124        hex = hex
125          .split("")
126          .map((c) => c + c)
127          .join("");
128      }
129
130      // Handle 8-digit hex (with alpha) - just use first 6
131      if (hex.length === 8) {
132        hex = hex.substring(0, 6);
133      }
134
135      if (hex.length !== 6) return null;
136
137      const num = parseInt(hex, 16);
138      if (isNaN(num)) return null;
139
140      return {
141        r: (num >> 16) & 255,
142        g: (num >> 8) & 255,
143        b: num & 255,
144      };
145    },
146
147    /**
148     * Convert HSL to RGB
149     * @param {number} h - hue (0-360)
150     * @param {number} s - saturation (0-100)
151     * @param {number} l - lightness (0-100)
152     * @returns {object} - {r, g, b}
153     */
154    hslToRgb(h, s, l) {
155      s /= 100;
156      l /= 100;
157
158      const c = (1 - Math.abs(2 * l - 1)) * s;
159      const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
160      const m = l - c / 2;
161
162      let r = 0,
163        g = 0,
164        b = 0;
165
166      if (h >= 0 && h < 60) {
167        r = c;
168        g = x;
169        b = 0;
170      } else if (h >= 60 && h < 120) {
171        r = x;
172        g = c;
173        b = 0;
174      } else if (h >= 120 && h < 180) {
175        r = 0;
176        g = c;
177        b = x;
178      } else if (h >= 180 && h < 240) {
179        r = 0;
180        g = x;
181        b = c;
182      } else if (h >= 240 && h < 300) {
183        r = x;
184        g = 0;
185        b = c;
186      } else if (h >= 300 && h < 360) {
187        r = c;
188        g = 0;
189        b = x;
190      }
191
192      return {
193        r: Math.round((r + m) * 255),
194        g: Math.round((g + m) * 255),
195        b: Math.round((b + m) * 255),
196      };
197    },
198
199    /**
200     * Convert RGB to HSL
201     * @param {number} r - red (0-255)
202     * @param {number} g - green (0-255)
203     * @param {number} b - blue (0-255)
204     * @returns {object} - {h, s, l}
205     */
206    rgbToHsl(r, g, b) {
207      r /= 255;
208      g /= 255;
209      b /= 255;
210
211      const max = Math.max(r, g, b);
212      const min = Math.min(r, g, b);
213      let h = 0;
214      let s = 0;
215      const l = (max + min) / 2;
216
217      if (max !== min) {
218        const d = max - min;
219        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
220
221        switch (max) {
222          case r:
223            h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
224            break;
225          case g:
226            h = ((b - r) / d + 2) / 6;
227            break;
228          case b:
229            h = ((r - g) / d + 4) / 6;
230            break;
231        }
232      }
233
234      return {
235        h: Math.round(h * 360),
236        s: Math.round(s * 100),
237        l: Math.round(l * 100),
238      };
239    },
240
241    /**
242     * Convert any color to HSL CSS variable format
243     * @param {string} color - any valid CSS color
244     * @returns {string|null} - "h, s%, l%" or null
245     */
246    toHslString(color) {
247      const rgb = this.parseColor(color);
248      if (!rgb) return null;
249
250      const hsl = this.rgbToHsl(rgb.r, rgb.g, rgb.b);
251      return `${hsl.h}, ${hsl.s}%, ${hsl.l}%`;
252    },
253  };
254
255  /**
256   * SpotlightCard Class
257   */
258  class SpotlightCard {
259    constructor(element, options = {}) {
260      this.element =
261        typeof element === "string" ? document.querySelector(element) : element;
262
263      if (!this.element) {
264        console.warn("SpotlightCard: Element not found");
265        return;
266      }
267
268      this.options = {
269        color: null, // Single color for non-gradient modes
270        gradientColors: [], // Array of colors for gradient modes
271        size: "md",
272        intensity: "medium",
273        variant: "default",
274        mode: "hover",
275        autoplay: false,
276        autoplayType: "circle",
277        autoplaySpeed: "normal",
278        disabled: false,
279        cardBg: null, // Card background color
280        cardBorder: null, // Card border color
281        cardRadius: null, // Card border radius
282        onMouseEnter: null,
283        onMouseLeave: null,
284        onMouseMove: null,
285        ...options,
286      };
287
288      this._rafId = null;
289      this._autoplayRafId = null;
290      this._autoplayStartTime = null;
291      this._isHovering = false;
292
293      this._handleMouseMove = this._handleMouseMove.bind(this);
294      this._handleMouseEnter = this._handleMouseEnter.bind(this);
295      this._handleMouseLeave = this._handleMouseLeave.bind(this);
296      this._animateAutoplay = this._animateAutoplay.bind(this);
297
298      this._init();
299    }
300
301    _init() {
302      this.element.classList.add("spotlight-card");
303
304      // Sync border-radius: if not explicitly provided, read from computed style
305      // so the CSS variable matches any externally applied border-radius (e.g. Divi module settings)
306      if (!this.options.cardRadius) {
307        const computedRadius = getComputedStyle(this.element).borderRadius;
308        if (computedRadius && computedRadius !== "0px") {
309          this.options.cardRadius = computedRadius;
310        }
311      }
312
313      this._applyOptions();
314      this._addEventListeners();
315
316      if (this.options.autoplay) {
317        this._startAutoplay();
318      }
319    }
320
321    /**
322     * Apply color as CSS variable in HSL format
323     * @param {string} color - The color value
324     */
325    _applyColor(color) {
326      if (color) {
327        const hslString = ColorUtils.toHslString(color);
328        if (hslString) {
329          this.element.style.setProperty("--spotlight-color", hslString);
330        }
331      } else {
332        this.element.style.removeProperty("--spotlight-color");
333      }
334    }
335
336    /**
337     * Apply gradient colors as CSS variables
338     * @param {Array<string>} colors - Array of color values
339     */
340    _applyGradientColors(colors) {
341      if (!colors || colors.length === 0) {
342        // Clear all gradient color variables
343        for (let i = 1; i <= 10; i++) {
344          this.element.style.removeProperty(`--spotlight-gradient-${i}`);
345        }
346        this.element.style.removeProperty("--spotlight-gradient-count");
347        return;
348      }
349
350      // Set the count of gradient colors
351      this.element.style.setProperty(
352        "--spotlight-gradient-count",
353        colors.length,
354      );
355
356      // Apply each color
357      colors.forEach((color, index) => {
358        const hslString = ColorUtils.toHslString(color);
359        if (hslString) {
360          this.element.style.setProperty(
361            `--spotlight-gradient-${index + 1}`,
362            hslString,
363          );
364        }
365      });
366
367      // Clear any extra variables
368      for (let i = colors.length + 1; i <= 10; i++) {
369        this.element.style.removeProperty(`--spotlight-gradient-${i}`);
370      }
371
372      // Build and apply the gradient
373      this._buildGradient(colors);
374    }
375
376    /**
377     * Build gradient CSS from colors array
378     * @param {Array<string>} colors - Array of color values
379     */
380    _buildGradient(colors) {
381      if (!colors || colors.length === 0) return;
382
383      const hslColors = colors
384        .map((c) => ColorUtils.toHslString(c))
385        .filter(Boolean);
386      if (hslColors.length === 0) return;
387
388      // Store for use in CSS
389      this.element.setAttribute("data-gradient-colors", hslColors.join("|"));
390    }
391
392    /**
393     * Apply card styling options
394     */
395    _applyCardStyles() {
396      const { cardBg, cardBorder, cardRadius } = this.options;
397
398      if (cardBg) {
399        this.element.style.setProperty("--spotlight-card-bg", cardBg);
400      } else {
401        this.element.style.removeProperty("--spotlight-card-bg");
402      }
403
404      if (cardBorder) {
405        this.element.style.setProperty("--spotlight-card-border", cardBorder);
406      } else {
407        this.element.style.removeProperty("--spotlight-card-border");
408      }
409
410      if (cardRadius) {
411        this.element.style.setProperty("--spotlight-card-radius", cardRadius);
412      } else {
413        this.element.style.removeProperty("--spotlight-card-radius");
414      }
415    }
416
417    _applyOptions() {
418      const {
419        color,
420        gradientColors,
421        size,
422        intensity,
423        variant,
424        mode,
425        autoplay,
426        autoplayType,
427        autoplaySpeed,
428        disabled,
429      } = this.options;
430
431      // Set data attributes (using setter methods for size and intensity to handle custom values)
432      this.setSize(size);
433      this.setIntensity(intensity);
434      this.element.setAttribute("data-variant", variant);
435      this.element.setAttribute("data-mode", mode);
436      this.element.setAttribute("data-autoplay", autoplay);
437      this.element.setAttribute("data-autoplay-type", autoplayType);
438      this.element.setAttribute("data-autoplay-speed", autoplaySpeed);
439      this.element.setAttribute("data-disabled", disabled);
440
441      // Apply colors
442      this._applyColor(color);
443      this._applyGradientColors(gradientColors);
444      this._applyCardStyles();
445
446      // Toggle classes based on variant and mode
447      this.element.classList.toggle(
448        "spotlight-card--proximity",
449        mode === "proximity",
450      );
451      this.element.classList.toggle("spotlight-card--autoplay", autoplay);
452      this.element.classList.toggle(
453        "spotlight-card--border",
454        variant === "border",
455      );
456      this.element.classList.toggle(
457        "spotlight-card--gradient",
458        variant === "gradient",
459      );
460      this.element.classList.toggle(
461        "spotlight-card--gradient-border",
462        variant === "gradient-border",
463      );
464    }
465
466    _addEventListeners() {
467      this.element.addEventListener("mouseenter", this._handleMouseEnter);
468      this.element.addEventListener("mouseleave", this._handleMouseLeave);
469      this.element.addEventListener("mousemove", this._handleMouseMove);
470
471      if (this.options.mode === "proximity") {
472        document.addEventListener("mousemove", this._handleMouseMove);
473      }
474    }
475
476    _removeEventListeners() {
477      document.removeEventListener("mousemove", this._handleMouseMove);
478      this.element.removeEventListener("mousemove", this._handleMouseMove);
479      this.element.removeEventListener("mouseenter", this._handleMouseEnter);
480      this.element.removeEventListener("mouseleave", this._handleMouseLeave);
481
482      if (this._rafId) {
483        cancelAnimationFrame(this._rafId);
484        this._rafId = null;
485      }
486    }
487
488    _handleMouseMove(e) {
489      if (this.options.disabled) return;
490
491      if (this._rafId) cancelAnimationFrame(this._rafId);
492
493      this._rafId = requestAnimationFrame(() => {
494        const rect = this.element.getBoundingClientRect();
495        const x = e.clientX - rect.left;
496        const y = e.clientY - rect.top;
497
498        this.element.style.setProperty("--mouse-x", `${x}px`);
499        this.element.style.setProperty("--mouse-y", `${y}px`);
500
501        if (typeof this.options.onMouseMove === "function") {
502          this.options.onMouseMove({ x, y, event: e });
503        }
504      });
505    }
506
507    _handleMouseEnter(e) {
508      if (this.options.disabled) return;
509
510      this._isHovering = true;
511
512      // Pause autoplay on hover
513      if (this.options.autoplay) {
514        this._stopAutoplay();
515      }
516
517      if (typeof this.options.onMouseEnter === "function") {
518        this.options.onMouseEnter(e);
519      }
520    }
521
522    _handleMouseLeave(e) {
523      if (this.options.disabled) return;
524
525      this._isHovering = false;
526
527      // Resume autoplay on leave
528      if (this.options.autoplay) {
529        this._startAutoplay();
530      } else {
531        this.element.style.setProperty("--mouse-x", "50%");
532        this.element.style.setProperty("--mouse-y", "50%");
533      }
534
535      if (typeof this.options.onMouseLeave === "function") {
536        this.options.onMouseLeave(e);
537      }
538    }
539
540    // Smooth autoplay animation using requestAnimationFrame
541    _animateAutoplay(timestamp) {
542      if (!this.options.autoplay || this._isHovering || this.options.disabled) {
543        return;
544      }
545
546      if (!this._autoplayStartTime) {
547        this._autoplayStartTime = timestamp;
548      }
549
550      // Support both preset names and numeric millisecond values
551      let duration;
552      if (typeof this.options.autoplaySpeed === "number") {
553        duration = this.options.autoplaySpeed;
554      } else {
555        duration =
556          autoplaySpeeds[this.options.autoplaySpeed] || autoplaySpeeds.normal;
557      }
558
559      const elapsed = timestamp - this._autoplayStartTime;
560      const progress = (elapsed % duration) / duration; // 0 to 1
561
562      const pathFn =
563        autoplayPaths[this.options.autoplayType] || autoplayPaths.circle;
564      const { x, y } = pathFn(progress);
565
566      const rect = this.element.getBoundingClientRect();
567      const pixelX = (x / 100) * rect.width;
568      const pixelY = (y / 100) * rect.height;
569
570      this.element.style.setProperty("--mouse-x", `${pixelX}px`);
571      this.element.style.setProperty("--mouse-y", `${pixelY}px`);
572
573      this._autoplayRafId = requestAnimationFrame(this._animateAutoplay);
574    }
575
576    _startAutoplay() {
577      if (this._autoplayRafId) return;
578      this._autoplayStartTime = null;
579      this._autoplayRafId = requestAnimationFrame(this._animateAutoplay);
580    }
581
582    _stopAutoplay() {
583      if (this._autoplayRafId) {
584        cancelAnimationFrame(this._autoplayRafId);
585        this._autoplayRafId = null;
586      }
587    }
588
589    // Public Methods
590    setOptions(newOptions) {
591      const modeChanged =
592        newOptions.mode && newOptions.mode !== this.options.mode;
593      const autoplayChanged =
594        newOptions.autoplay !== undefined &&
595        newOptions.autoplay !== this.options.autoplay;
596
597      this.options = { ...this.options, ...newOptions };
598      this._applyOptions();
599
600      if (modeChanged) {
601        this._removeEventListeners();
602        this._addEventListeners();
603      }
604
605      if (autoplayChanged) {
606        if (this.options.autoplay) {
607          this._startAutoplay();
608        } else {
609          this._stopAutoplay();
610        }
611      }
612    }
613
614    /**
615     * Set spotlight color (for non-gradient modes)
616     * @param {string} color - Any CSS color format (hex, rgb, hsl, named)
617     */
618    setColor(color) {
619      this.options.color = color;
620      this._applyColor(color);
621    }
622
623    /**
624     * Set gradient colors (for gradient variants)
625     * @param {Array<string>} colors - Array of colors in any CSS format
626     */
627    setGradientColors(colors) {
628      this.options.gradientColors = colors;
629      this._applyGradientColors(colors);
630    }
631
632    /**
633     * Set card background color
634     * @param {string} color - Any CSS color format
635     */
636    setCardBg(color) {
637      this.options.cardBg = color;
638      if (color) {
639        this.element.style.setProperty("--spotlight-card-bg", color);
640      } else {
641        this.element.style.removeProperty("--spotlight-card-bg");
642      }
643    }
644
645    /**
646     * Set card border color
647     * @param {string} color - Any CSS color format
648     */
649    setCardBorder(color) {
650      this.options.cardBorder = color;
651      if (color) {
652        this.element.style.setProperty("--spotlight-card-border", color);
653      } else {
654        this.element.style.removeProperty("--spotlight-card-border");
655      }
656    }
657
658    /**
659     * Set card border radius
660     * @param {string} radius - CSS border-radius value
661     */
662    setCardRadius(radius) {
663      this.options.cardRadius = radius;
664      if (radius) {
665        this.element.style.setProperty("--spotlight-card-radius", radius);
666      } else {
667        this.element.style.removeProperty("--spotlight-card-radius");
668      }
669    }
670
671    setSize(size) {
672      this.options.size = size;
673      // Check if it's a custom pixel value (number or string with px)
674      if (
675        typeof size === "number" ||
676        (typeof size === "string" && /^\d+/.test(size))
677      ) {
678        const pxValue =
679          typeof size === "number"
680            ? `${size}px`
681            : size.endsWith("px")
682            ? size
683            : `${size}px`;
684        this.element.style.setProperty("--spotlight-size", pxValue);
685        this.element.setAttribute("data-size", "custom");
686        this.element.setAttribute("data-size-value", pxValue);
687      } else {
688        // Preset size (sm, md, lg, xl)
689        this.element.style.removeProperty("--spotlight-size");
690        this.element.setAttribute("data-size", size);
691        this.element.removeAttribute("data-size-value");
692      }
693    }
694
695    setIntensity(intensity) {
696      this.options.intensity = intensity;
697      // Check if it's a custom value (number between 0-1 or 0-100)
698      if (
699        typeof intensity === "number" ||
700        (typeof intensity === "string" && /^[\d.]+$/.test(intensity))
701      ) {
702        let value = parseFloat(intensity);
703        // Normalize to 0-1 range if given as percentage (0-100)
704        if (value > 1) value = value / 100;
705        // Clamp between 0 and 1
706        value = Math.max(0, Math.min(1, value));
707
708        // Calculate opacity values based on intensity
709        const spotlightOpacity = (value * 0.35).toFixed(3); // 0 to 0.35
710        const borderOpacity = (value * 0.9).toFixed(3); // 0 to 0.9
711
712        this.element.style.setProperty("--spotlight-opacity", spotlightOpacity);
713        this.element.style.setProperty(
714          "--spotlight-border-opacity",
715          borderOpacity,
716        );
717        this.element.setAttribute("data-intensity", "custom");
718        this.element.setAttribute("data-intensity-value", value);
719      } else {
720        // Preset intensity (low, medium, high)
721        this.element.style.removeProperty("--spotlight-opacity");
722        this.element.style.removeProperty("--spotlight-border-opacity");
723        this.element.setAttribute("data-intensity", intensity);
724        this.element.removeAttribute("data-intensity-value");
725      }
726    }
727
728    setVariant(variant) {
729      this.options.variant = variant;
730      this.element.setAttribute("data-variant", variant);
731      this.element.classList.toggle(
732        "spotlight-card--border",
733        variant === "border",
734      );
735      this.element.classList.toggle(
736        "spotlight-card--gradient",
737        variant === "gradient",
738      );
739      this.element.classList.toggle(
740        "spotlight-card--gradient-border",
741        variant === "gradient-border",
742      );
743    }
744
745    setAutoplay(enabled, type = "circle", speed = 3000) {
746      this.options.autoplay = enabled;
747      this.options.autoplayType = type;
748      this.options.autoplaySpeed = speed;
749
750      this.element.setAttribute("data-autoplay", enabled);
751      this.element.setAttribute("data-autoplay-type", type);
752      // Store speed value (works with both preset names and numbers)
753      this.element.setAttribute("data-autoplay-speed", speed);
754      this.element.classList.toggle("spotlight-card--autoplay", enabled);
755
756      if (enabled && !this._isHovering) {
757        this._startAutoplay();
758      } else {
759        this._stopAutoplay();
760      }
761    }
762
763    enable() {
764      this.options.disabled = false;
765      this.element.setAttribute("data-disabled", "false");
766      if (this.options.autoplay) {
767        this._startAutoplay();
768      }
769    }
770
771    disable() {
772      this.options.disabled = true;
773      this.element.setAttribute("data-disabled", "true");
774      this._stopAutoplay();
775    }
776
777    toggle() {
778      if (this.options.disabled) {
779        this.enable();
780      } else {
781        this.disable();
782      }
783      return this.options.disabled;
784    }
785
786    destroy() {
787      this._removeEventListeners();
788      this._stopAutoplay();
789
790      this.element.classList.remove(
791        "spotlight-card",
792        "spotlight-card--proximity",
793        "spotlight-card--autoplay",
794        "spotlight-card--border",
795        "spotlight-card--gradient",
796        "spotlight-card--gradient-border",
797      );
798
799      const attrs = [
800        "size",
801        "intensity",
802        "variant",
803        "mode",
804        "autoplay",
805        "autoplay-type",
806        "autoplay-speed",
807        "disabled",
808        "gradient-colors",
809      ];
810      attrs.forEach((attr) => this.element.removeAttribute(`data-${attr}`));
811
812      // Remove all CSS custom properties
813      const propsToRemove = [
814        "--mouse-x",
815        "--mouse-y",
816        "--spotlight-color",
817        "--spotlight-card-bg",
818        "--spotlight-card-border",
819        "--spotlight-card-radius",
820        "--spotlight-gradient-count",
821      ];
822      propsToRemove.forEach((prop) => this.element.style.removeProperty(prop));
823
824      // Remove gradient color variables
825      for (let i = 1; i <= 10; i++) {
826        this.element.style.removeProperty(`--spotlight-gradient-${i}`);
827      }
828    }
829  }
830
831  /**
832   * SpotlightCardManager Class
833   */
834  class SpotlightCardManager {
835    constructor() {
836      this.cards = new Map();
837    }
838
839    init(selector = "[data-spotlight-card]", options = {}) {
840      const elements = document.querySelectorAll(selector);
841      const instances = [];
842
843      elements.forEach((element) => {
844        if (this.cards.has(element)) {
845          instances.push(this.cards.get(element));
846          return;
847        }
848
849        // Parse data attributes
850        const speedAttr = element.getAttribute("data-spotlight-autoplay-speed");
851        const parsedSpeed = speedAttr ? parseInt(speedAttr, 10) : NaN;
852        const dataOptions = {
853          color: element.getAttribute("data-spotlight-color"),
854          size: element.getAttribute("data-spotlight-size"),
855          intensity: element.getAttribute("data-spotlight-intensity"),
856          variant: element.getAttribute("data-spotlight-variant"),
857          mode: element.getAttribute("data-spotlight-mode"),
858          autoplay: element.getAttribute("data-spotlight-autoplay") === "true",
859          autoplayType: element.getAttribute("data-spotlight-autoplay-type"),
860          autoplaySpeed: !isNaN(parsedSpeed) ? parsedSpeed : speedAttr,
861          disabled: element.getAttribute("data-spotlight-disabled") === "true",
862          cardBg: element.getAttribute("data-spotlight-card-bg"),
863          cardBorder: element.getAttribute("data-spotlight-card-border"),
864          cardRadius: element.getAttribute("data-spotlight-card-radius"),
865        };
866
867        // Parse gradient colors (comma-separated)
868        const gradientColorsAttr = element.getAttribute(
869          "data-spotlight-gradient-colors",
870        );
871        if (gradientColorsAttr) {
872          dataOptions.gradientColors = gradientColorsAttr
873            .split(",")
874            .map((c) => c.trim())
875            .filter(Boolean);
876        }
877
878        // Remove null/undefined values
879        Object.keys(dataOptions).forEach((key) => {
880          if (dataOptions[key] === null || dataOptions[key] === undefined) {
881            delete dataOptions[key];
882          }
883        });
884
885        const mergedOptions = { ...options, ...dataOptions };
886        const instance = new SpotlightCard(element, mergedOptions);
887
888        this.cards.set(element, instance);
889        instances.push(instance);
890      });
891
892      return instances;
893    }
894
895    get(element) {
896      return this.cards.get(element);
897    }
898
899    has(element) {
900      return this.cards.has(element);
901    }
902
903    remove(element) {
904      const instance = this.cards.get(element);
905      if (instance) {
906        instance.destroy();
907        this.cards.delete(element);
908      }
909    }
910
911    getAll() {
912      return Array.from(this.cards.values());
913    }
914
915    destroyAll() {
916      this.cards.forEach((instance) => instance.destroy());
917      this.cards.clear();
918    }
919
920    setAllOptions(options) {
921      this.cards.forEach((instance) => instance.setOptions(options));
922    }
923  }
924
925  function autoInit() {
926    const manager = new SpotlightCardManager();
927    manager.init();
928
929    // Support [data-spotlight-children] — init spotlight on each child matching selector
930    document.querySelectorAll('[data-spotlight-children]').forEach((parent) => {
931      const childSelector = parent.getAttribute('data-spotlight-children');
932      let configStr = parent.getAttribute('data-spotlight-config');
933      let config = {};
934      try { config = JSON.parse(configStr || '{}'); } catch(e) {}
935
936      // Parse config into SpotlightCard options
937      const opts = {};
938      if (config['data-spotlight-color']) opts.color = config['data-spotlight-color'];
939      if (config['data-spotlight-gradient-colors']) {
940        opts.gradientColors = config['data-spotlight-gradient-colors'].split(',').map(s => s.trim());
941      }
942      if (config['data-spotlight-size']) opts.size = config['data-spotlight-size'];
943      if (config['data-spotlight-intensity']) opts.intensity = config['data-spotlight-intensity'];
944      if (config['data-spotlight-variant']) opts.variant = config['data-spotlight-variant'];
945      if (config['data-spotlight-mode']) opts.mode = config['data-spotlight-mode'];
946      if (config['data-spotlight-autoplay'] === 'true') {
947        opts.autoplay = true;
948        if (config['data-spotlight-autoplay-type']) opts.autoplayType = config['data-spotlight-autoplay-type'];
949        if (config['data-spotlight-autoplay-speed']) opts.autoplaySpeed = parseInt(config['data-spotlight-autoplay-speed']);
950      }
951
952      const children = parent.querySelectorAll(childSelector);
953      children.forEach((child) => {
954        if (child.style.position === '' || child.style.position === 'static') {
955          child.style.position = 'relative';
956        }
957        child.style.overflow = 'hidden';
958        const instance = new SpotlightCard(child, opts);
959        manager.cards.set(child, instance);
960      });
961    });
962
963    global.spotlightCardManager = manager;
964  }
965
966  // Expose for external use
967  global.SpotlightCard = SpotlightCard;
968  global.SpotlightCardManager = SpotlightCardManager;
969  global.SpotlightColorUtils = ColorUtils;
970
971  if (document.readyState === "loading") {
972    document.addEventListener("DOMContentLoaded", autoInit);
973  } else {
974    autoInit();
975  }
976})(typeof window !== "undefined" ? window : this);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.