From 46d681c6b825d21b3f698d793c4e630c687d90ad Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 21 May 2026 21:41:53 +0000
Subject: [PATCH] =Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start

---
 inc/managers/IconsManager.php |  758 ++++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 607 insertions(+), 151 deletions(-)

diff --git a/inc/managers/IconsManager.php b/inc/managers/IconsManager.php
index d3710d0..f54120b 100644
--- a/inc/managers/IconsManager.php
+++ b/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,78 +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()
+
+	/**
+	 * Constructor now takes source parameter
+	 */
+	private function __construct(string $source)
 	{
-		$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';
+		$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();
+
+		// 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();
+	}
 
 
-		$this->usedIcons = get_option(BASE.'used_icons', []);
-		$this->includeIcons();
-		// Register hooks only once
-		$this->registerHooks();
+
+	/**
+	 * Register all custom icons (runs once)
+	 */
+	protected function registerCustomIcons(): void
+	{
+		$icons = array_merge(apply_filters('jvbRegisterCustomIcons', []), ['syncing' => JVB_DIR . '/assets/icons/cloud-sync-thin.svg',
+			'alphabetical' => JVB_DIR . '/assets/icons/alphabetical.svg']);
+
+		// 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 includeIcons():array
+	protected function processCustomIconsArray(array $icons): array
 	{
-		$icons = get_option(BASE.'includeIcons');
-//		$icons = false;
-		if (!$icons) {
-			$icons = $this->addIncludeIcons();
-		}
-		$include = apply_filters('jvbIncludeIcons', []);
-		$add = array_filter($include, function($addIt) use ($icons) {
-			return !in_array($addIt, $icons);
-		});
-		error_log('Adding icons: '.print_r($add, true));
-		if (!empty($add)) {
-			$this->addIncludeIcons($add);
+		$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 $icons;
+		return $out;
 	}
 
-	protected function addIncludeIcons(array $add = []) {
-		$icons = get_option(BASE.'includeIcons');
-		if (!$icons) {
-			$icons = [
+	/**
+	 * 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
+	{
+		$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',
@@ -91,9 +159,11 @@
 				'share-fat',
 				'trash',
 				'star',
+				'alphabetical',
 				['name' => 'star-half', 'style' => 'fill'],
 				['name' => 'star', 'style' => 'fill'],
-				//FORMATTING
+			],
+			'forms' => [
 				'copy',
 				'paragraph',
 				'text-h-one',
@@ -119,47 +189,177 @@
 				'file-doc',
 				'file-txt',
 				'file-xls',
-			];
-			$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);
+			],
+//			'dash' => [
+//
+//			]
+		];
 
-			update_option(BASE.'includeIcons', $icons);
-			update_option(BASE.'icons_needs_update', true);
+
+		// Add icons from content/taxonomy/user configs (like old behavior)
+		$configIcons = $this->getIconsFromConfigs();
+		if (!empty($configIcons)) {
+			$defaults['icons'] = array_merge($defaults['icons'], $configIcons);
 		}
-		$add = apply_filters('jvbIncludeIcons', $add);
-		$add = array_filter($add, function($addIt) use ($icons) {
-			return !in_array($addIt, $icons);
-		});
-		if (!empty($add)) {
-			$icons = array_merge($add, $icons);
-			update_option(BASE.'usedIcons', $icons);
-			update_option(BASE.'icons_needs_update', true);
+
+		// 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]);
 		}
-		return $icons;
+		if (!empty($icons)) {
+			$this->include($icons);
+		}
 	}
 
-	protected function addMap():void
+	/**
+	 * Get icons from Registrar instances
+	 *
+	 */
+	protected function getIconsFromConfigs(): array
+	{
+		$icons = [];
+		$registered = Registrar::getRegistered();
+
+		foreach ($registered as $type) {
+			$registrar = Registrar::getInstance($type);
+			$icons[] = $registrar->getIcon();
+		}
+
+
+		return array_unique(array_filter($icons));
+	}
+
+	/**
+	 * Public method to include icons in this source
+	 */
+	public function include(array $icons): self
+	{
+		$processed = $this->processIconArray($icons);
+		$changed = false;
+
+		foreach ($processed as $style => $names) {
+			if (!isset($this->icons[$style])) {
+				$this->icons[$style] = [];
+			}
+
+			foreach ($names as $name) {
+				// Skip if already in this source
+				if (in_array($name, $this->icons[$style])) {
+					continue;
+				}
+
+				// 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;
+	}
+
+	/**
+	 * 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);
@@ -169,35 +369,128 @@
 	}
 
 	/**
-	 * Register WordPress hooks
+	 * Register global hooks (only once)
 	 */
-	protected function registerHooks(): void
+	protected function registerGlobalHooks(): void
 	{
-		add_action('init', [$this, 'checkCSS']);
-		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_DIR.'/assets/css/icons.css';
+		$css_dir = JVB_CHILD_DIR.'/assets/css/';
 
-		if (file_put_contents($css_path, $css) !== false) {
-			CacheManager::updateTimestamp('icons');
-		} else {
-			error_log('[IconsManager]Could not write css.');
+		if (!file_exists($css_dir)) {
+			wp_mkdir_p($css_dir);
+		}
+
+		// Load all icons from database option
+		$allIcons = get_option(BASE.'usedIcons', []);
+
+		// If no specific sources provided, regenerate all
+		if (empty($sourcesToUpdate)) {
+			$sourcesToUpdate = array_fill_keys(array_keys($allIcons), true);
+		}
+
+		// 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");
+			}
 		}
 	}
 
@@ -226,57 +519,66 @@
 	 *   - '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
 	{
-		if (!array_key_exists('style', $options)) {
-			$options['style'] = $this->style;
+		if (empty($name)) {
+			//No icon requested
+			return '';
 		}
-		$name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name;
+		$style = $options['style'] ?? $this->style;
+		$name = $this->map[$name] ?? $name;
 
 		// Validate icon exists
-		if (!$this->iconExists($name, $options['style'])) {
+		if (!$this->iconExists($name, $style)) {
 			error_log('[IconsManager] Icon not found: ' . $name);
 			return '';
 		}
 
-		$style = $options['style'] ?? $this->style;
+		// Track usage - only if not already tracked
+		if (!isset($this->icons[$style])) {
+			$this->icons[$style] = [];
+		}
 
-		// Track icon usage
-		$this->trackIconUsage($name, $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();
+			}
+		}
 
-		// Build classes
-		$classes = ['icon', 'icon-' . $name];
-		if (!empty($options['class'])) {
+		// Build icon HTML (same as before)
+		$styleClass = ($style !== $this->style) ? '-'.substr($style, 0, 2) : '';
+		$classes = ['icon', 'icon-' . $name.$styleClass];
+
+		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
-	{
-		if (!array_key_exists($style, $this->usedIcons)) {
-			$this->usedIcons[$style] = [];
+		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;
-			update_option(BASE.'used_icons', $this->usedIcons);
-			// Flag for regeneration on next init
-			update_option(BASE.'icons_needs_update', true);
-
-			// Clear cache
-			$this->cache->delete('icon_styles_css');
+		if (isset($options['size'])) {
+			$attrs['style'] = sprintf('--icon-size: %dpx;', absint($options['size']));
 		}
+
+		$attr_string = '';
+		foreach ($attrs as $key => $value) {
+			$attr_string .= sprintf(' %s="%s"', $key, $value);
+		}
+
+		return sprintf('<i%s></i>', $attr_string);
 	}
 
 	/**
@@ -323,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;
@@ -342,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_URL.'assets/css/icons.css',
+		wp_register_style(
+			$handle,
+			JVB_CHILD_URL . "assets/css/{$this->source}.css",
 			[],
 			$timestamp
 		);
@@ -369,18 +666,28 @@
 	protected function generateIconCSS(): string
 	{
 		$css = '';
-		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);
 	}
 
@@ -395,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 '';
+			});
 	}
 
 	/**
@@ -427,8 +743,148 @@
 	 */
 	public function clearIconCache(): void
 	{
-		delete_option(BASE . 'icon_usage_list'); // Clear DB option
-		$this->cache->delete('icon_styles_css');
-		CacheManager::updateTimestamp('icons');
+		delete_option(BASE . 'icon_usage_list'); // Legacy
+		delete_option(BASE.'usedIcons');
+		delete_option(BASE.'iconMap');
+
+		// Clear cache for all sources
+		foreach (self::$instances as $source => $instance) {
+			$instance->cache->forget('icon_styles_css');
+		}
+	}
+
+	protected function archiveCurrentVersion(string $css): void
+	{
+		$history = $this->getVersionHistory();
+
+		$icon_count = 0;
+		foreach ($this->icons as $style => $names) {
+			$icon_count += count($names);
+		}
+
+		$newEntry = [
+			'css' => $css,
+			'iconList' => $this->icons,
+			'timestamp' => time(),
+			'icon_count' => $icon_count,
+			'size' => strlen($css),
+			'size_formatted' => size_format(strlen($css), 2)
+		];
+
+		array_unshift($history, $newEntry);
+
+		if (count($history) > self::MAX_VERSIONS) {
+			$history = array_slice($history, 0, self::MAX_VERSIONS);
+		}
+
+		update_option(BASE.'icon_css_history_' . $this->source, $history);
+	}
+
+	public function getVersionHistory(): array
+	{
+		return get_option(BASE.'icon_css_history_' . $this->source, []);
+	}
+
+	public function restoreVersion(int $timestamp): bool
+	{
+		$history = $this->getVersionHistory();
+
+		foreach ($history as $entry) {
+			if ($entry['timestamp'] === $timestamp) {
+				$css_path = JVB_CHILD_DIR . '/assets/css/' . $this->source . '.css';
+
+				// Archive current before restoring
+				$current_css = file_get_contents($css_path);
+				if ($current_css !== false) {
+					$this->archiveCurrentVersion($current_css);
+				}
+
+				// Restore the version
+				if (file_put_contents($css_path, $entry['css']) !== false) {
+					$this->icons = $entry['iconList'];
+					$this->saveIcons();
+					Cache::touch('icons_' . $this->source);
+					return true;
+				}
+
+				return false;
+			}
+		}
+
+		error_log("[IconsManager] Version {$timestamp} not found in history for source {$this->source}");
+		return false;
+	}
+
+	public function forceRefresh(): void
+	{
+		$this->clearIconCache();
+		$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
+	{
+		if (empty($timestamps)) {
+			return false;
+		}
+
+		$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)) {
+				foreach ($entry['iconList'] as $style => $icons) {
+					if (!isset($merged_icons[$style])) {
+						$merged_icons[$style] = [];
+					}
+					// Merge and keep unique
+					$merged_icons[$style] = array_unique(
+						array_merge($merged_icons[$style], $icons)
+					);
+				}
+			}
+		}
+
+		if (empty($merged_icons)) {
+			error_log('[IconsManager] No icons found in selected versions');
+			return false;
+		}
+
+		// Archive current version
+		$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->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]);
 	}
 }

--
Gitblit v1.10.0