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->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' ]); $this->usedIcons = get_option(BASE.'used_icons', []); $this->includeIcons(); // Register hooks only once $this->registerHooks(); } /** * Include icons via filter (for JS usage, etc.) */ protected function includeIcons():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); } return $icons; } protected function addIncludeIcons(array $add = []) { $icons = get_option(BASE.'includeIcons'); if (!$icons) { $icons = [ 'check-circle', 'close-circle', 'cloud-slash', 'exclamation-mark', 'cloud-arrow-down', 'cloud-arrow-up', 'cloud-check', 'cloud-slash', 'cloud-warning', 'syncing', 'cloud-x', 'arrows-clockwise', 'share-fat', 'trash', 'star', ['name' => 'star-half', 'style' => 'fill'], ['name' => 'star', 'style' => 'fill'], //FORMATTING 'copy', 'paragraph', 'text-h-one', 'text-h-two', 'text-h-three', 'text-h-four', 'text-h-five', 'text-h-six', ['name' =>'text-b', 'style' => 'fill'], 'text-italic', 'text-underline', 'text-strikethrough', 'list-dashes', 'list-numbers', 'text-align-left', 'text-align-center', 'text-align-right', // 'text-align-justify', 'link', //FILE ICONS 'file-pdf', 'file-csv', '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); update_option(BASE.'includeIcons', $icons); update_option(BASE.'icons_needs_update', true); } $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); } return $icons; } protected function addMap():void { $map = get_option(BASE.'iconMap'); if (!$map) { $map = []; if (Features::forSite()->has('referrals')){ $map['referrals'] = 'hand-heart'; } if (Features::forSite()->has('dashboard')){ $map['dash'] = 'door'; } if (Features::forSite()->has('magicLink')){ $map['magicLink'] = 'magic-wand'; } if (Features::hasAnyIntegration()) { $map['integrations'] = 'plugs-connected'; } update_option(BASE.'iconMap', $map); } $this->map = apply_filters('jvbMapIcons', $map); } /** * Register WordPress hooks */ protected function registerHooks(): void { add_action('init', [$this, 'checkCSS']); add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']); add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']); } public function checkCSS():void { // update_option(BASE.'icons_needs_update', true); if (get_option(BASE.'icons_needs_update', false)) { error_log('Regenerating CSS'); delete_option(BASE.'icons_needs_update'); $this->regenerateCSS(); } } protected function regenerateCSS(): void { error_log('[IconsManager]:regenerateCSS'); $css = $this->generateIconCSS(); $css_path = JVB_DIR.'/assets/css/icons.css'; if (file_put_contents($css_path, $css) !== false) { CacheManager::updateTimestamp('icons'); } else { error_log('[IconsManager]Could not write css.'); } } /** * Prevent cloning */ private function __clone() {} /** * Prevent unserialization */ public function __wakeup() { throw new \Exception("Cannot unserialize singleton"); } /** * Get an icon element * * @param string $name Icon name (e.g., 'heart', 'calendar') * @param array $options Options array: * - 'style' => 'regular'|'bold'|'fill'|etc. * - 'label' => 'Accessible label' (for standalone icons) * - 'decorative' => true (for icons next to text) * - 'class' => 'additional classes' * - 'size' => 24 (for custom sizing via inline style) * @return string HTML icon element */ public function getIcon(string $name, array $options = []): string { if (!array_key_exists('style', $options)) { $options['style'] = $this->style; } $name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name; // Validate icon exists if (!$this->iconExists($name, $options['style'])) { error_log('[IconsManager] Icon not found: ' . $name); return ''; } $style = $options['style'] ?? $this->style; // Track icon usage $this->trackIconUsage($name, $style); // Build classes $classes = ['icon', 'icon-' . $name]; if (!empty($options['class'])) { $classes[] = $options['class']; } $attrs = ['class="' . esc_attr(implode(' ', $classes)) . '"']; $attrs[] = 'aria-hidden="true"'; return ''; } /** * 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 (!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'); } } /** * Check if icon file exists */ protected function iconExists(string $name, ?string $style = null): bool { if (!$style) { $style = $this->style; } // Check custom icons first if (array_key_exists($name, $this->customIcons)) { return file_exists($this->customIcons[$name]); } // Check standard icons $filepath = $this->buildFilePath($name, $style); return file_exists($filepath); } /** * Build file path for icon */ protected function buildFilePath(string $name, ?string $style = null): string { if (!$style) { $style = $this->style; } // Custom icons (absolute path provided) if (array_key_exists($name, $this->customIcons)) { return $this->customIcons[$name]; } // Standard SVG icons in /assets/icons/ if (str_ends_with($name, '.svg')) { return JVB_DIR . '/assets/icons/' . $name; } $name = ($style === 'regular') ? $name : $name . '-' . $style; // Phosphor icons with style variants return JVB_DIR . '/assets/phosphor-icons/' . $style . '/' . $name . '.svg'; } /** * Get raw SVG content for CSS mask-image */ protected function getRawSvg(string $name, ?string $style = null): ?string { if (!$style) { $style = $this->style; } $filepath = $this->buildFilePath($name, $style); if (!file_exists($filepath)) { return null; } $svg = file_get_contents($filepath); if ($svg === false) { return null; } // Clean up SVG for CSS usage $svg = preg_replace("/([\n\t]+)/", ' ', $svg); $svg = preg_replace('/>\s*<', $svg); $svg = trim($svg); return $svg; } /** * Enqueue icon styles via REST endpoint */ public function enqueueIconStyles(): void { $timestamp = CacheManager::getTimestamp('icons'); wp_enqueue_style( 'jvb-icons', JVB_URL.'assets/css/icons.css', [], $timestamp ); } /** * Generate CSS from icon list */ protected function generateIconCSS(): string { $css = ''; foreach ($this->usedIcons as $style => $icons) { $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : ''; foreach ($icons as $icon) { $svg = $this->getEncodedSVG($icon, $style); if ($svg !== '') { $css .= ".icon-{$icon}{$styleClass}{"; $css .= "--icon:url('data:image/svg+xml;base64,{$svg}');"; $css .= "}"; } } } return $this->minifyCss($css); } protected function minifyCSS(string $css): string { // Remove comments $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css); // Remove whitespace $css = preg_replace('/\s+/', ' ', $css); // Remove spaces around specific characters $css = preg_replace('/\s*([:;{}])\s*/', '$1', $css); return trim($css); } public function getCSSIcon(string $icon, ?string $style=null):string { if (!$style) { $style = $this->style; } $svg = $this->getEncodedSVG($icon, $style); if ($svg !== '') { return "data:image/svg+xml;base64,{$svg}"; } return ''; } 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 ''; }); } /** * Clear icon cache (useful for development/debugging) */ public function clearIconCache(): void { delete_option(BASE . 'icon_usage_list'); // Clear DB option $this->cache->delete('icon_styles_css'); CacheManager::updateTimestamp('icons'); } }