Jake Vanderwerf
7 days ago 46d681c6b825d21b3f698d793c4e630c687d90ad
inc/managers/IconsManager.php
@@ -1,7 +1,8 @@
<?php
namespace JVBase\managers;
use JVBase\utility\Features;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
if (!defined('ABSPATH')) {
   exit;
@@ -9,79 +10,145 @@
class IconsManager
{
   protected static ?IconsManager $instance = null;
   protected CacheManager $cache;
   // Static array holding all source instances
   protected static array $instances = [];
   // Static storage for all custom icons across sources
   protected static array $customIconsRegistry = [];
   // Instance-specific properties
   protected string $source;
   protected array $icons = []; // Icons for THIS source [style => [names]]
   protected Cache $cache;
   protected string $style = 'regular';
   protected array $styles = ['regular', 'bold', 'duotone', 'fill', 'light', 'thin'];
   // Custom icons registered via filter
   protected array $customIcons = [];
   protected array $usedIcons = [];
   protected array $customIcons = []; // Custom icons for THIS source
   protected array $map = [];
   protected const MAX_VERSIONS = 5;
   /**
    * Get singleton instance
    * Factory method - get or create instance for a source
    */
   public static function getInstance(): IconsManager
   public static function for(string $source = 'icons'): IconsManager
   {
      if (self::$instance === null) {
         self::$instance = new self();
      if (!isset(self::$instances[$source])) {
         self::$instances[$source] = new self($source);
      }
      return self::$instance;
      return self::$instances[$source];
   }
   private function __construct()
   {
      $this->cache = CacheManager::for('icons', WEEK_IN_SECONDS);
      $this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles))
         ? JVB_SITE['icons']
         : 'regular';
   /**
    * Constructor now takes source parameter
    */
   private function __construct(string $source)
   {
      $this->source = $source;
      $this->cache = Cache::for('icons_' . $source, WEEK_IN_SECONDS);
      $this->style = Site::icon();
      $this->addMap();
      // Allow custom icon registration
      $this->customIcons = apply_filters('jvbRegisterCustomIcons', [
         'syncing'      => JVB_DIR .'/assets/icons/cloud-sync-thin.svg',
         'alphabetical' => JVB_DIR.'/assets/icons/alphabetical.svg'
      ]);
      // Register custom icons only once for all sources
      if ($source === 'icons') {
         $this->registerCustomIcons();
      }
      // Load custom icons for THIS source
      $this->loadCustomIconsForSource();
      $this->usedIcons = get_option(BASE.'usedIcons', []);
      $this->includeIcons();
      // Track custom icons for CSS generation
      $this->trackCustomIcons();
      // Register hooks only once
      $this->registerHooks();
      // Load stored icons for this source
      $this->loadStoredIcons();
      if (empty($this->icons)) {
         $this->includeIcons();
      }
      // Register global hooks only once (first instance)
      if (count(self::$instances) === 1) {
         $this->registerGlobalHooks();
      }
      // Register instance's hooks (every instance)
      $this->registerInstanceHooks();
   }
   /**
    * Ensure custom icons are tracked for CSS generation
    * Register all custom icons (runs once)
    */
   protected function trackCustomIcons(): void
   protected function registerCustomIcons(): void
   {
      if (empty($this->customIcons)) {
         return;
      }
      $icons = array_merge(apply_filters('jvbRegisterCustomIcons', []), ['syncing' => JVB_DIR . '/assets/icons/cloud-sync-thin.svg',
         'alphabetical' => JVB_DIR . '/assets/icons/alphabetical.svg']);
      foreach ($this->customIcons as $name => $path) {
         $this->trackIconUsage($name, $this->style);
      }
      // Process and store in static property so all instances can access
      self::$customIconsRegistry = $this->processCustomIconsArray($icons);
   }
   /**
    * Include icons via filter (for JS usage, etc.)
    * Process custom icons array into source-grouped format
    */
   protected function processCustomIconsArray(array $icons): array
   {
      $out = [];
      foreach ($icons as $name => $source) {
         if (!file_exists($source)) {
            error_log('[IconsManager] No file exists for custom Icon: '.$name);
            continue;
         }
         $out[$name] = $source;
      }
      return $out;
   }
   /**
    * Load custom icons for this instance's source
    */
   protected function loadCustomIconsForSource(): void
   {
      $this->customIcons = self::$customIconsRegistry;
//    foreach ($this->customIcons as $name => $path) {
//       if (!isset($this->icons[$this->style])) {
//          $this->icons[$this->style] = [];
//       }
//       if (!in_array($name, $this->icons[$this->style])) {
//          $this->icons[$this->style][] = $name;
//       }
//    }
   }
   /**
    * Load previously stored icons for this source
    */
   protected function loadStoredIcons(): void
   {
      $allIcons = get_option(BASE.'usedIcons', []);
      $storedIcons = $allIcons[$this->source] ?? [];
      // Merge stored icons with any existing icons (like custom icons)
      foreach ($storedIcons as $style => $names) {
         if (!isset($this->icons[$style])) {
            $this->icons[$style] = [];
         }
         $this->icons[$style] = array_unique(array_merge($this->icons[$style], $names));
      }
   }
   protected function includeIcons():void
   {
      $icons = get_option(BASE.'includeIcons');
      if (!$icons) {
         $icons = [
      $defaults = [
         'icons' => [
            'google-logo',
            'apple-logo',
            'check-circle',
            'close-circle',
            'faders-horizontal',
            'cloud-slash',
            'exclamation-mark',
            'cloud-arrow-down',
            'caret-down',
            'cloud-arrow-up',
            'cloud-check',
            'cloud-slash',
@@ -92,9 +159,11 @@
            'share-fat',
            'trash',
            'star',
            'alphabetical',
            ['name' => 'star-half', 'style' => 'fill'],
            ['name' => 'star', 'style' => 'fill'],
            //FORMATTING
         ],
         'forms' => [
            'copy',
            'paragraph',
            'text-h-one',
@@ -120,105 +189,177 @@
            'file-doc',
            'file-txt',
            'file-xls',
         ];
         ],
//       'dash' => [
//
//       ]
      ];
         $check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
         foreach ($check as $constant) {
            foreach ($constant as $key => $value) {
               if (array_key_exists('icon', $value) && !in_array($value['icon'], $icons)) {
                  $icons[] = $value['icon'];
               }
            }
         }
         $icons = apply_filters('jvbIncludeIcons', $icons);
         $icons = $this->maybePrefixIcons($icons);
         update_option(BASE.'includeIcons', $icons);
      // Add icons from content/taxonomy/user configs (like old behavior)
      $configIcons = $this->getIconsFromConfigs();
      if (!empty($configIcons)) {
         $defaults['icons'] = array_merge($defaults['icons'], $configIcons);
      }
      // Ensure icons are in the correct format (handle legacy data)
      if (!$this->isIconsArrayPrefixed($icons)) {
         $icons = $this->maybePrefixIcons($icons);
         update_option(BASE.'includeIcons', $icons);
      // Allow filtering per source (extensibility)
      $icons = apply_filters("jvbIncludeIcons_{$this->source}", $defaults[$this->source] ?? []);
      // Also allow filtering all sources at once
      $allIcons = apply_filters('jvbIncludeIcons', $defaults);
      if (isset($allIcons[$this->source])) {
         $icons = array_merge($icons, $allIcons[$this->source]);
      }
      $additional = apply_filters('jvbIncludeIcons', []);
      if (!empty($additional)) {
         $additional = $this->maybePrefixIcons($additional);
         $merged = $this->mergeUsedIcons($icons, $additional);
         if ($icons != $merged) {
            update_option(BASE.'includeIcons', $merged);
            $icons = $merged;
         }
      }
      foreach ($icons as $style => $theIcons) {
         foreach($theIcons as $icon) {
            $this->trackIconUsage($icon, $style);
         }
      if (!empty($icons)) {
         $this->include($icons);
      }
   }
   /**
    * Check if icons array is in the prefixed format [style => [icons]]
    * Get icons from Registrar instances
    *
    */
   protected function isIconsArrayPrefixed(array $icons): bool
   protected function getIconsFromConfigs(): array
   {
      if (empty($icons)) {
         return true;
      $icons = [];
      $registered = Registrar::getRegistered();
      foreach ($registered as $type) {
         $registrar = Registrar::getInstance($type);
         $icons[] = $registrar->getIcon();
      }
      // Check if first key is a valid style name
      $first_key = array_key_first($icons);
      if (!in_array($first_key, $this->styles)) {
         return false;
      }
      // Check if first value is an array
      return is_array($icons[$first_key]);
      return array_unique(array_filter($icons));
   }
   protected function maybePrefixIcons(array $icons):array
   /**
    * Public method to include icons in this source
    */
   public function include(array $icons): self
   {
      $out = [];
      foreach ($icons as $icon) {
         if (is_array($icon) && array_key_exists('style', $icon)) {
            if (!array_key_exists($icon['style'], $out)) {
               $out[$icon['style']] = [];
            }
            if (!in_array($icon['name'], $out[$icon['style']])) {
               $out[$icon['style']][] = $icon['name'];
            }
         } elseif(is_array($icon)) {
            $icon = $icon['name'];
      $processed = $this->processIconArray($icons);
      $changed = false;
      foreach ($processed as $style => $names) {
         if (!isset($this->icons[$style])) {
            $this->icons[$style] = [];
         }
         if (!is_array($icon)) {
            if (!array_key_exists($this->style, $out)) {
               $out[$this->style] = [];
         foreach ($names as $name) {
            // Skip if already in this source
            if (in_array($name, $this->icons[$style])) {
               continue;
            }
            if (!in_array($icon, $out[$this->style])){
               $out[$this->style][] = $icon;
            // Skip if already in main 'icons' source
            if ($this->iconExistsInMainSource($name, $style)) {
               error_log("[IconsManager] Skipping '{$name}' in '{$this->source}' - already in 'icons' source");
               continue;
            }
            $this->icons[$style][] = $name;
            $changed = true;
         }
      }
      // Only save if something actually changed
      if ($changed) {
         $this->saveIcons();
      }
      return $this;
   }
   /**
    * Process icon array into [style => [names]] format
    */
   protected function processIconArray(array $icons): array
   {
      $out = [];
      foreach ($icons as $icon) {
         if (is_array($icon) && isset($icon['style'])) {
            $style = $icon['style'];
            $name = $icon['name'];
         } else {
            $style = $this->style;
            $name = is_array($icon) ? $icon['name'] : $icon;
         }
         if (!isset($out[$style])) {
            $out[$style] = [];
         }
         if (!in_array($name, $out[$style])) {
            $out[$style][] = $name;
         }
      }
      return $out;
   }
   protected function addMap():void
   /**
    * Save all icons across all instances
    */
   protected function saveIcons(): void
   {
      $allIcons = [];
      foreach (self::$instances as $source => $instance) {
         $allIcons[$source] = $instance->icons;
      }
      update_option(BASE.'usedIcons', $allIcons);
      // Track WHICH source needs updating
      $needsUpdate = get_option(BASE.'icons_needs_update', []);
      if (!is_array($needsUpdate)) {
         $needsUpdate = [];
      }
      $needsUpdate[$this->source] = true;
      update_option(BASE.'icons_needs_update', $needsUpdate);
   }
   /**
    * Check if icon exists in other sources
    */
   protected function checkDuplicateAcrossInstances(string $name, string $style): void
   {
      $foundIn = [];
      foreach (self::$instances as $source => $instance) {
         if (isset($instance->icons[$style]) && in_array($name, $instance->icons[$style])) {
            $foundIn[] = $source;
         }
      }
      if (count($foundIn) > 1) {
         error_log(sprintf(
            '[IconsManager] Warning: Icon "%s" (%s) is registered in multiple sources: %s. Consider consolidating to avoid duplicate CSS output.',
            $name,
            $style,
            implode(', ', $foundIn)
         ));
      }
   }
   protected function addMap(): void
   {
      $map = get_option(BASE.'iconMap');
      if (!$map) {
         $map = [];
         if (Features::forSite()->has('referrals')){
         $map = [
            'seo' => 'robot'
         ];
         if (Site::has('referrals')) {
            $map['referrals'] = 'hand-heart';
         }
         if (Features::forSite()->has('dashboard')){
         if (Site::has('dashboard')) {
            $map['dash'] = 'door';
         }
         if (Features::forSite()->has('magicLink')){
         if (Site::has('magicLink')) {
            $map['magicLink'] = 'magic-wand';
         }
         if (Features::hasAnyIntegration()) {
         if (Site::hasAnyIntegration()) {
            $map['integrations'] = 'plugs-connected';
         }
         update_option(BASE.'iconMap', $map);
@@ -228,44 +369,128 @@
   }
   /**
    * Register WordPress hooks
    * Register global hooks (only once)
    */
   protected function registerHooks(): void
   protected function registerGlobalHooks(): void
   {
      add_action('init', [$this, 'includeIcons'], 1);
      add_action('init', [$this, 'checkCSS'], 10);
      add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
      add_action('wp_loaded', [self::class, 'checkCSS']);
      add_action('shutdown', [self::class, 'checkCSS']);
   }
   /**
    * Register instance-specific hooks (every instance)
    */
   protected function registerInstanceHooks(): void
   {
      // Register this source's stylesheet
      add_action('init', [$this, 'registerStyle'], 11);
      // Auto-enqueue base icons on front-end
      if ($this->source === 'icons') {
         add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
      }
      // Auto-enqueue all in admin
      add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']);
   }
   public function checkCSS():void
   public function enqueueIconStyles():void
   {
//    update_option(BASE.'icons_needs_update', true);
      if (get_option(BASE.'icons_needs_update', false)) {
         error_log('Regenerating CSS');
//    if (file_exists(JVB_CHILD_URL . "assets/css/{$this->source}.css")){
         wp_enqueue_style('jvb-icons-'.$this->source);
//    }
   }
   public static function checkCSS(): void
   {
      $needsUpdate = get_option(BASE.'icons_needs_update', []);
      if (!empty($needsUpdate)) {
         error_log('Regenerating CSS for sources: ' . implode(', ', array_keys($needsUpdate)));
         delete_option(BASE.'icons_needs_update');
         $this->regenerateCSS();
         self::regenerateAllCSS($needsUpdate);
      }
   }
   protected function regenerateCSS(): void
   public static function regenerateAllCSS(array $sourcesToUpdate = []): void
   {
      error_log('[IconsManager]:regenerateCSS');
      $css = $this->generateIconCSS();
      $css_path = JVB_CHILD_DIR.'/assets/css/';
      if (!file_exists($css_path)) {
         wp_mkdir_p($css_path);
      $css_dir = JVB_CHILD_DIR.'/assets/css/';
      if (!file_exists($css_dir)) {
         wp_mkdir_p($css_dir);
      }
      $css_path .= '/icons.css';
      // Load all icons from database option
      $allIcons = get_option(BASE.'usedIcons', []);
      // Archive current version before overwriting
      $this->archiveCurrentVersion($css);
      // If no specific sources provided, regenerate all
      if (empty($sourcesToUpdate)) {
         $sourcesToUpdate = array_fill_keys(array_keys($allIcons), true);
      }
      if (file_put_contents($css_path, $css) !== false) {
         CacheManager::updateTimestamp('icons');
      } else {
         error_log('[IconsManager]Could not write css.');
      // Generate CSS for each source that needs it
      foreach ($sourcesToUpdate as $source => $needsUpdate) {
         if (!$needsUpdate || !isset($allIcons[$source])) {
            continue;
         }
         // Get or create instance for this source
         $instance = self::for($source);
         // Temporarily set icons from database
         $originalIcons = $instance->icons;
         $instance->icons = $allIcons[$source];
         $css = $instance->generateIconCSS();
         $css_path = $css_dir . $source . '.css';
         $instance->archiveCurrentVersion($css);
         if (file_put_contents($css_path, $css) !== false) {
            Cache::touch('icons_' . $source);
            error_log("[IconsManager] Updated {$source}.css");
         } else {
            error_log("[IconsManager] Could not write {$source}.css");
         }
         // Restore original icons
         $instance->icons = $originalIcons;
      }
   }
   protected function regenerateCSS(array $sourcesToUpdate = []): void
   {
      error_log('[IconsManager]:regenerateCSS');
      $css_dir = JVB_CHILD_DIR.'/assets/css/';
      if (!file_exists($css_dir)) {
         wp_mkdir_p($css_dir);
      }
      // If no specific sources provided, regenerate all
      if (empty($sourcesToUpdate)) {
         $sourcesToUpdate = array_fill_keys(array_keys(self::$instances), true);
      }
      // Generate CSS only for sources that need it
      foreach (self::$instances as $source => $instance) {
         if (!isset($sourcesToUpdate[$source])) {
            continue; // Skip this source
         }
         $css = $instance->generateIconCSS();
         $css_path = $css_dir . $source . '.css';
         // Archive current version before overwriting
         $instance->archiveCurrentVersion($css);
         if (file_put_contents($css_path, $css) !== false) {
            Cache::touch('icons_' . $source);
            error_log("[IconsManager] Updated {$source}.css");
         } else {
            error_log("[IconsManager] Could not write {$source}.css");
         }
      }
   }
@@ -294,10 +519,14 @@
    *   - 'size' => 24 (for custom sizing via inline style)
    * @return string HTML icon element
    */
   public function getIcon(string $name, array $options = []): string
   public function get(string $name, array $options = []): string
   {
      $style = array_key_exists('style', $options) ? $options['style'] :$this->style;
      $name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name;
      if (empty($name)) {
         //No icon requested
         return '';
      }
      $style = $options['style'] ?? $this->style;
      $name = $this->map[$name] ?? $name;
      // Validate icon exists
      if (!$this->iconExists($name, $style)) {
@@ -305,56 +534,51 @@
         return '';
      }
      // Track usage - only if not already tracked
      if (!isset($this->icons[$style])) {
         $this->icons[$style] = [];
      }
      if (!in_array($name, $this->icons[$style])) {
         // Check if it's already in main source (for non-main sources)
         if ($this->iconExistsInMainSource($name, $style)) {
            // Don't add to this source, but still render the icon
            // The CSS from icons.css will handle it
         } else {
            // Add to this source
            $this->icons[$style][] = $name;
            $this->checkDuplicateAcrossInstances($name, $style);
            $this->saveIcons();
         }
      }
      // Track icon usage
      $this->trackIconUsage($name, $style);
      $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
      // Build classes
      // Build icon HTML (same as before)
      $styleClass = ($style !== $this->style) ? '-'.substr($style, 0, 2) : '';
      $classes = ['icon', 'icon-' . $name.$styleClass];
      if (!empty($options['class'])) {
      if (isset($options['class'])) {
         $classes[] = $options['class'];
      }
      $attrs = ['class' => implode(' ', $classes)];
      $attrs = ['class="' . esc_attr(implode(' ', $classes)) . '"'];
      $attrs[] = 'aria-hidden="true"';
      return '<i ' . implode(' ', $attrs) . '></i>';
   }
   /**
    * Track icon usage for CSS generation
    */
   protected function trackIconUsage(string $name, string $style): void
   {
      $needsUpdate = false;
      if (!array_key_exists($style, $this->usedIcons)) {
         $this->usedIcons[$style] = [];
         $needsUpdate = true;
      if (isset($options['label'])) {
         $attrs['aria-label'] = esc_attr($options['label']);
         $attrs['role'] = 'img';
      } elseif (isset($options['decorative']) && $options['decorative']) {
         $attrs['aria-hidden'] = 'true';
      }
      if (!in_array($name, $this->usedIcons[$style])) {
         $this->usedIcons[$style][] = $name;
         $needsUpdate = true;
      if (isset($options['size'])) {
         $attrs['style'] = sprintf('--icon-size: %dpx;', absint($options['size']));
      }
      if ($needsUpdate) {
         // Merge with existing option to never lose icons
         $existing = get_option(BASE.'usedIcons', []);
         $merged = $this->mergeUsedIcons($existing, $this->usedIcons);
         update_option(BASE.'usedIcons', $merged);
         // Flag for regeneration on next init
         update_option(BASE.'icons_needs_update', true);
         // Clear cache
         $this->cache->delete('icon_styles_css');
      $attr_string = '';
      foreach ($attrs as $key => $value) {
         $attr_string .= sprintf(' %s="%s"', $key, $value);
      }
      return sprintf('<i%s></i>', $attr_string);
   }
   /**
@@ -401,7 +625,7 @@
   /**
    * Get raw SVG content for CSS mask-image
    */
   protected function getRawSvg(string $name, ?string $style = null): ?string
   public function getRawSvg(string $name, ?string $style = null): ?string
   {
      if (!$style) {
         $style = $this->style;
@@ -420,22 +644,17 @@
      // Clean up SVG for CSS usage
      $svg = preg_replace("/([\n\t]+)/", ' ', $svg);
      $svg = preg_replace('/>\s*</', '><', $svg);
      $svg = trim($svg);
      return $svg;
      return trim($svg);
   }
   /**
    * Enqueue icon styles via REST endpoint
    */
   public function enqueueIconStyles(): void
   public function registerStyle(): void
   {
      $timestamp = CacheManager::getTimestamp('icons');
      $timestamp = Cache::lastModified('icons_' . $this->source);
      $handle = 'jvb-icons-' . $this->source;
      wp_enqueue_style(
         'jvb-icons',
         JVB_CHILD_URL.'assets/css/icons.css',
      wp_register_style(
         $handle,
         JVB_CHILD_URL . "assets/css/{$this->source}.css",
         [],
         $timestamp
      );
@@ -447,48 +666,29 @@
   protected function generateIconCSS(): string
   {
      $css = '';
      $this->mergeUsedIcons();
      foreach ($this->usedIcons as $style => $icons) {
         $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
         foreach ($icons as $icon) {
      foreach ($this->icons as $style => $names) {
         $styleClass = ($style !== $this->style) ? '-'.substr($style, 0, 2) : '';
         foreach ($names as $icon) {
            $svg = $this->getEncodedSVG($icon, $style);
            if ($svg !== '') {
               if ($icon === 'caret-down') {
                  $css .= 'details summary::after,';
               } elseif ($icon === 'faders-horizontal') {
                  $css .= 'details.all-filters summary::after,';
               } elseif ($icon === 'link') {
                  $css .= 'input[type=url],';
               } elseif ($icon === apply_filters('jvbSeparatorLogo', 'logo')) {
                  $css .= 'hr.logo::before,';
               }
               $css .= ".icon-{$icon}{$styleClass}{";
               $css .= "--icon:url('data:image/svg+xml;base64,{$svg}');";
               $css .= "}";
            }
         }
      }
      return $this->minifyCss($css);
   }
   protected function mergeUsedIcons(array|bool $oldIcons = true, array|bool $newIcons = true):array
   {
      $set = false;
      if ($oldIcons === true) {
         $oldIcons = $this->usedIcons;
         $set = true;
      }
      if ($newIcons === true) {
         $history = $this->getVersionHistory();
         $newIcons = (count($history) > 0) ? $history[0]['iconList'] : [];
      }
      foreach ($newIcons as $style => $icons) {
         if (!isset($oldIcons[$style])) {
            //Style  doesn't exist in previous set, add the whole thing
            $oldIcons[$style] = $icons;
         } else {
            $oldIcons[$style] = array_unique(
               array_merge($oldIcons[$style], $icons)
            );
         }
      }
      if ($set) {
         $this->usedIcons = $oldIcons;
         update_option(BASE.'usedIcons', $oldIcons);
      }
      return $oldIcons;
      return $this->minifyCss($css);
   }
   protected function minifyCSS(string $css): string
@@ -502,31 +702,40 @@
      return trim($css);
   }
   public function getCSSIcon(string $icon, ?string $style=null):string
   public function getCSSIcon(string $icon, ?string $style = null): string
   {
      if (!$style) {
         $style = $this->style;
      }
      $icon = $this->map[$icon] ?? $icon;
      // Validate icon exists
      if (!$this->iconExists($icon, $style)) {
         error_log('[IconsManager] Icon not found: ' . $icon);
         return '';
      }
      $svg = $this->getEncodedSVG($icon, $style);
      if ($svg !== '') {
         return "data:image/svg+xml;base64,{$svg}";
      }
      return '';
   }
   public function getEncodedSVG(string $icon, ?string $style = null):string
   public function getEncodedSVG(string $icon, ?string $style = null): string
   {
      if (!$style) {
         $style = $this->style;
      }
      return $this->cache->remember($style.$icon,
      function () use ($icon, $style) {
         $svg = $this->getRawSvg($icon, $style);
         if ($svg) {
            return base64_encode($svg);
         }
         return '';
      });
         function () use ($icon, $style) {
            $svg = $this->getRawSvg($icon, $style);
            if ($svg) {
               return base64_encode($svg);
            }
            return '';
         });
   }
   /**
@@ -534,12 +743,14 @@
    */
   public function clearIconCache(): void
   {
      delete_option(BASE . 'icon_usage_list'); // Clear DB option
      delete_option(BASE . 'icon_usage_list'); // Legacy
      delete_option(BASE.'usedIcons');
      delete_option(BASE.'includeIcons');
      delete_option(BASE.'iconMap');
      $this->cache->delete('icon_styles_css');
      CacheManager::updateTimestamp('icons');
      // Clear cache for all sources
      foreach (self::$instances as $source => $instance) {
         $instance->cache->forget('icon_styles_css');
      }
   }
   protected function archiveCurrentVersion(string $css): void
@@ -547,13 +758,13 @@
      $history = $this->getVersionHistory();
      $icon_count = 0;
      foreach ($this->usedIcons as $style => $icons) {
         $icon_count += count($icons);
      foreach ($this->icons as $style => $names) {
         $icon_count += count($names);
      }
      $newEntry = [
         'css' => $css,
         'iconList' => $this->usedIcons,
         'iconList' => $this->icons,
         'timestamp' => time(),
         'icon_count' => $icon_count,
         'size' => strlen($css),
@@ -566,12 +777,12 @@
         $history = array_slice($history, 0, self::MAX_VERSIONS);
      }
      update_option(BASE.'icon_css_history', $history);
      update_option(BASE.'icon_css_history_' . $this->source, $history);
   }
   public function getVersionHistory(): array
   {
      return get_option(BASE.'icon_css_history', []);
      return get_option(BASE.'icon_css_history_' . $this->source, []);
   }
   public function restoreVersion(int $timestamp): bool
@@ -580,7 +791,7 @@
      foreach ($history as $entry) {
         if ($entry['timestamp'] === $timestamp) {
            $css_path = JVB_DIR . '/assets/css/icons.css';
            $css_path = JVB_CHILD_DIR . '/assets/css/' . $this->source . '.css';
            // Archive current before restoring
            $current_css = file_get_contents($css_path);
@@ -590,9 +801,9 @@
            // Restore the version
            if (file_put_contents($css_path, $entry['css']) !== false) {
               $this->usedIcons = $entry['iconList'];
               update_option(BASE.'usedIcons', $this->usedIcons);
               CacheManager::updateTimestamp('icons');
               $this->icons = $entry['iconList'];
               $this->saveIcons();
               Cache::touch('icons_' . $this->source);
               return true;
            }
@@ -600,15 +811,20 @@
         }
      }
      error_log("[IconsManager] Version {$timestamp} not found in history");
      error_log("[IconsManager] Version {$timestamp} not found in history for source {$this->source}");
      return false;
   }
   public function forceRefresh(): void
   {
      $this->clearIconCache();
      update_option(BASE.'icons_needs_update', true);
      CacheManager::updateTimestamp('icons');
      $needsUpdate = get_option(BASE.'icons_needs_update', []);
      if (!is_array($needsUpdate)) {
         $needsUpdate = [];
      }
      $needsUpdate[$this->source] = true;
      update_option(BASE.'icons_needs_update', $needsUpdate);
      Cache::touch('icons_' . $this->source);
   }
   public function mergeVersions(array $timestamps): bool
@@ -617,8 +833,9 @@
         return false;
      }
      $history = get_option(BASE.'icon_css_history', []);
      $history = get_option(BASE.'icon_css_history_' . $this->source, []);
      $merged_icons = [];
      // Collect icons from selected versions
      foreach ($history as $entry) {
         if (in_array($entry['timestamp'], $timestamps)) {
@@ -640,18 +857,34 @@
      }
      // Archive current version
      $current_css = file_get_contents(JVB_DIR . '/assets/css/icons.css');
      $current_css = file_get_contents(JVB_CHILD_DIR . '/assets/css/' . $this->source . '.css');
      if ($current_css !== false) {
         $this->archiveCurrentVersion($current_css);
      }
      // Update used icons and regenerate
      $this->usedIcons = $merged_icons;
      update_option(BASE.'usedIcons', $this->usedIcons);
      // Force regeneration
      $this->regenerateCSS();
      $this->icons = $merged_icons;
      $this->saveIcons();
      return true;
   }
   /**
    * Check if icon already exists in the main 'icons' source
    */
   protected function iconExistsInMainSource(string $name, string $style): bool
   {
      // If this IS the main source, no need to check
      if ($this->source === 'icons') {
         return false;
      }
      // Check if main icons source exists
      if (!isset(self::$instances['icons'])) {
         return false;
      }
      $mainIcons = self::$instances['icons']->icons;
      return isset($mainIcons[$style]) && in_array($name, $mainIcons[$style]);
   }
}