'my-plugin-slug', * 'plugin_name' => 'My Plugin', * 'page_title' => 'AIFORYA — My Plugin', * 'menu_label' => 'My Plugin', * 'capability' => 'manage_options', * 'callback' => 'my_plugin_render_page', * 'plugin_file' => __FILE__, * 'version' => MY_PLUGIN_VERSION, * 'plugin_image' => plugins_url( 'assets/images/banner.jpg', __FILE__ ), // optional * ]); * } */ if ( ! class_exists( 'AIFORYA_Hub', false ) ) { class AIFORYA_Hub { /** @var array> */ private static array $registered_plugins = []; private static bool $menu_created = false; /** Icône AIFORYA blanche en base64 — menu sidebar WP */ private static string $icon_b64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4NzcgNTE2Ij48cGF0aCBkPSJNNjE0Ljc1LDMzMi4xM2MtMjMuNjk3LDI4LjAyIC01MS40ODcsNjAuOTAyIC02NS42Niw3OC42OGMtMjQuMzMsMzAuNTE5IC00Ny43MzIsNDQuMjc5IC02NC41OCw1MS4wMTNjLTE2LjgzNSw2LjcyOSAtMzIuMTY5LDEwLjcwOSAtNTAuNzQ2LDEwLjcwOWwtMjE0LjE0MiwtMC4zMDZsMTEuNTEsLTE0LjE5YzYuOTE2LC03LjM1IDMzLjAzLC0zNC41MzggMzMuMDMsLTM0LjUzOGMxOS45MTIsLTI0LjU0OCAzNi4wNjcsLTMwLjU4NSA4My4xNDMsLTMwLjU4NWw2MS4yODcsLTBjNDcuMTYsLTAgNjcuODUzLC0xNi42MDMgOTQuNTQ3LC00OC45MDJjMjIuNDEyLC0yNy4xMTggNjMuMTU1LC04MC44NzYgODMuOTYsLTEwNC44ODljMTguMjYyLC0yMS4wNzggMzMuOTE0LC0zMS43MTMgNTMuOTQ3LC0zMC44OTRjMTAuNjY5LDAuNDM2IDI1LjE2MSwzLjAxOSAzNi43ODUsMTYuMjU2bDYwLjUwNyw3Ni40NTJjMC42MzQsMC42ODcgMS4yNjUsMS4zOTUgMS44OTQsMi4xMjRsMTAwLjQ1NiwxMzAuMDc5bC00NS41MjIsMC4wNjljLTEuMDAzLC0wLjE2OSAtMi4wMzEsLTAuMjU3IC0zLjA3OCwtMC4yNTdjLTgxLjk1LC0wIC0xMDAuNjE5LDAuNjI3IC0xMzEuMDE3LC0zOC45OTVjLTEyLjI2NiwtMTUuOTg5IC0zMi40NzUsLTQwLjkyOSAtNDYuMzIxLC02MS44MjZabS00OTIuNDU3LDEzOS40NGwtOTIuNjc2LC0wLjYyNmMwLDAgNDIuMTM4LC01Mi42NiAxMDMuMzk0LC0xMTkuODcxYzI0Ljg3LC0yNS41MzYgMzUuMzk2LC00My40NzQgNTQuNTIzLC01MS43OThjNy4wMTQsLTMuMDUyIDIxLjM5NCwtNC40NDIgMzEuOTgyLC01LjNsMTE5Ljc2MSwwYzQ1LjExMSwwIDgxLjA4OCwtMjUuMDkxIDEwOS43MTcsLTYwLjI4NmMyNC40MTIsLTMwLjAxMSA2OS40MTYsLTkwLjkzNSA4OS4zODksLTEwOS44OWMxMC4xNTQsLTkuNjM2IDMwLjYwNSwtMTkuNzkxIDQ5LjI0NCwtOS4xMzJjOC4xMzIsNC42NTEgMTUuMTU3LDExLjQ5NCAyMi41ODIsMjAuMDQ1bDMxLjg5NCw0Mi41NDVjLTM0Ljk3OSwtMjEuMzIyIC01OS42MSw2LjA3MiAtNzMuMzE1LDIwLjcwM2MtMjIuMDMzLDIzLjUyMyAtNzIuMjIyLDkwLjgxIC05NS4wMTUsMTE4LjM4OWMtMTguNTEyLDIyLjM5OSAtMzIuNDc1LDM0Ljc1MyAtNjUuMTgxLDM0Ljc1M2wtNjYuMzAxLC0wYy02NC40NDEsLTAgLTg1LjQ5MywxMS40MjMgLTExMi43NDksNDUuMDI1bC0yNy43NzIsMzQuMjM4Yy0xNC4xMDksMTQuODYxIC0yNy43NTMsMjUuNzExIC00MC4wODQsMzIuNzMyYy04LjgyMSw1LjAyMyAtMTYuMjk1LDguNDczIC0zOS4zOTMsOC40NzNabTEwNy4xMywtMjE5Ljg2NmM3MC4wNTUsLTc2Ljc4NyAxMzUuMjI1LC0xNDguMDkzIDE1MC41MzksLTE2NC40NDNjMzYuMTY1LC0zOC42MSA0Ny40NjYsLTU0LjY5NyA5My40MjUsLTU0LjY5N2w4OC45MDUsLTBjMCwtMCAtMTE3LjU1MywxNDIuNTg5IC0xNDIuODY4LDE3My43MWMtMjEuMSwyNS45NCAtNDYuODk5LDQ1Ljg5MSAtODAuMTQ3LDQ1Ljg5MWwtODUuNzE2LC0wYy0xLjEwOSwtMC4yNDMgLTIuMjUyLC0wLjM4NSAtMy40MjMsLTAuNDE4Yy03LjUyNSwtMC4yMTMgLTE0LjM5OSwtMC4yMjYgLTIwLjcxNSwtMC4wNDNaIiBmaWxsPSJ3aGl0ZSIvPjwvc3ZnPg=='; /** * Traductions intégrées — pas besoin de .mo pour le Hub. * @return array */ private static function i18n(): array { $locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale(); $lang = substr( $locale, 0, 2 ); $strings = [ 'hub_title' => 'AIFORYA Hub', 'hub_subtitle' => 'Central manager for your AIFORYA plugins', 'plugins_label' => 'extension', 'plugins_label_plural' => 'extensions', 'active' => 'Active', 'open' => 'Ouvrir', 'no_plugins' => 'No AIFORYA plugin registered.', ]; $translations = [ 'en' => [ 'hub_subtitle' => 'Central management for all your AIFORYA plugins', 'plugins_label' => 'plugin', 'plugins_label_plural' => 'plugins', 'active' => 'Active', 'open' => 'Open', 'no_plugins' => 'No AIFORYA plugins registered yet.', ], 'de' => [ 'hub_subtitle' => 'Zentrale Verwaltung aller AIFORYA-Plugins', 'plugins_label' => 'Plugin', 'plugins_label_plural' => 'Plugins', 'active' => 'Aktiv', 'open' => 'Öffnen', 'no_plugins' => 'Noch keine AIFORYA-Plugins registriert.', ], 'es' => [ 'hub_subtitle' => 'Gestión central de todos sus plugins AIFORYA', 'plugins_label' => 'plugin', 'plugins_label_plural' => 'plugins', 'active' => 'Activo', 'open' => 'Abrir', 'no_plugins' => 'Ningún plugin AIFORYA registrado aún.', ], 'it' => [ 'hub_subtitle' => 'Gestione centrale di tutti i plugin AIFORYA', 'plugins_label' => 'plugin', 'plugins_label_plural' => 'plugin', 'active' => 'Attivo', 'open' => 'Apri', 'no_plugins' => 'Nessun plugin AIFORYA registrato.', ], 'nl' => [ 'hub_subtitle' => 'Centraal beheer van alle AIFORYA-plugins', 'plugins_label' => 'plugin', 'plugins_label_plural' => 'plugins', 'active' => 'Actief', 'open' => 'Openen', 'no_plugins' => 'Nog geen AIFORYA-plugins geregistreerd.', ], ]; if ( isset( $translations[ $lang ] ) ) { $strings = array_merge( $strings, $translations[ $lang ] ); } return $strings; } /** * Charge le catalogue de traduction PARTAGÉ (domain 'aiforya') du framework * vendor-aiforya/ : onglets Support & Avis, écran d'activation, skeleton, hub. * Partagé par toutes les extensions AIFORYA → chargé une seule fois par requête * (garde is_textdomain_loaded). Vit dans vendor-aiforya/languages/. * determine_locale() = langue de l'utilisateur en admin (profil), langue du site en front. */ public static function load_shared_textdomain(): void { $locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale(); $base = __DIR__ . '/languages/aiforya-' . $locale; // WP 6.5+ : format PHP (.l10n.php) prioritaire ; fallback .mo binaire. foreach ( array( '.l10n.php', '.mo' ) as $ext ) { if ( is_readable( $base . $ext ) ) { load_textdomain( 'aiforya-fg', $base . $ext ); return; } } } /** * Register a plugin page under the AIFORYA hub menu. * MUST be called from plugins_loaded hook — never from admin_menu. * * @param array $args { * @type string $slug Plugin page slug (unique) * @type string $plugin_name Display name * @type string $page_title Browser tab title * @type string $menu_label Submenu label * @type string $capability WP capability required * @type callable $callback Page render function * @type string $plugin_file Path to main plugin file (__FILE__) * @type string $version Plugin version string * @type string $plugin_image URL of product thumbnail image (optional, 1:1 ratio recommended) * @type string $description Short tagline shown in the Hub card (1-2 sentences max, like aiforya.fr) * @type string $category Product category label shown as pill in card (e.g. "Marketing & Ventes") * @type string $price Pricing label shown in card (e.g. "Sur devis", "9€/mois") * } */ public static function register_plugin_page( array $args ): void { $defaults = [ 'slug' => '', 'plugin_name' => '', 'page_title' => '', 'menu_label' => '', 'capability' => 'manage_options', 'callback' => '__return_empty_string', 'plugin_file' => '', 'version' => '', 'plugin_image' => '', 'description' => '', 'category' => '', 'price' => '', 'tabs' => [], ]; $args = array_merge( $defaults, $args ); // AIFORYA safety : accepter les alias WordPress add_menu_page pour les plugins // qui confondent les conventions (menu_title vs menu_label, page_title comme plugin_name). if ( empty( $args['menu_label'] ) && ! empty( $args['menu_title'] ) ) { $args['menu_label'] = $args['menu_title']; } if ( empty( $args['plugin_name'] ) && ! empty( $args['page_title'] ) ) { $args['plugin_name'] = $args['page_title']; } // Si menu_label toujours vide, derniere chance : utiliser page_title ou slug if ( empty( $args['menu_label'] ) ) { $args['menu_label'] = ! empty( $args['page_title'] ) ? $args['page_title'] : $args['slug']; } if ( empty( $args['slug'] ) ) { return; } self::$registered_plugins[ $args['slug'] ] = $args; if ( ! has_action( 'admin_menu', [ self::class, 'build_menu' ] ) ) { add_action( 'admin_menu', [ self::class, 'build_menu' ], 99 ); } // AIFORYA UI : auto-enqueue du CSS master sur les pages du plugin enregistre if ( ! has_action( 'admin_enqueue_scripts', [ self::class, 'enqueue_plugin_assets' ] ) ) { add_action( 'admin_enqueue_scripts', [ self::class, 'enqueue_plugin_assets' ] ); } } /** * Auto-enqueue le CSS master AIFORYA (assets/css/aiforya-ui.css) sur les * pages admin d'un plugin enregistre. Garantit que TOUTES les extensions * AIFORYA beneficient automatiquement du design system unifie. */ public static function enqueue_plugin_assets( $hook ): void { // Detection : est-on sur une page d'un plugin AIFORYA enregistre ? // Le slug vient du hook fourni par WordPress (`toplevel_page_` ou // `_page_`), pas de l'URL : aucune entrée utilisateur n'est lue ici. $page_slug = ''; if ( is_string( $hook ) && preg_match( '/_page_(.+)$/', $hook, $aiforya_m ) ) { $page_slug = sanitize_key( $aiforya_m[1] ); } if ( empty( $page_slug ) ) { return; } // Resolve plugin args : match direct du slug, ou si on est sur la hub page // enqueue pour TOUS les plugins enregistres (le CSS est partage). $plugin_args = null; if ( isset( self::$registered_plugins[ $page_slug ] ) ) { $plugin_args = self::$registered_plugins[ $page_slug ]; } elseif ( $page_slug === 'aiforya-hub' && ! empty( self::$registered_plugins ) ) { $plugin_args = reset( self::$registered_plugins ); } if ( $plugin_args === null ) { return; } // Resoudre le chemin du fichier main plugin : // 1. Si plugin_file fourni dans register_plugin_page -> utiliser // 2. Sinon, fallback WP_PLUGIN_DIR/{slug}/{slug}.php // 3. Sinon, deduire via debug_backtrace des callers $plugin_file = ''; if ( ! empty( $plugin_args['plugin_file'] ) && file_exists( $plugin_args['plugin_file'] ) ) { $plugin_file = $plugin_args['plugin_file']; } else { $slug = isset( $plugin_args['slug'] ) ? $plugin_args['slug'] : $page_slug; $candidate = WP_PLUGIN_DIR . '/' . $slug . '/' . $slug . '.php'; if ( file_exists( $candidate ) ) { $plugin_file = $candidate; } } if ( empty( $plugin_file ) ) { return; } // Enqueue aiforya-ui.css depuis les assets du plugin $css_path = dirname( $plugin_file ) . '/assets/css/aiforya-ui.css'; if ( file_exists( $css_path ) ) { wp_enqueue_style( 'aiforya-ui-bbf8d166', plugins_url( 'assets/css/aiforya-ui.css', $plugin_file ), array(), ! empty( $plugin_args['version'] ) ? $plugin_args['version'] : '1.0.0' ); } // CSS du hub : injecte par wp_add_inline_style() plutot que par une balise de // style en dur — exigence wordpress.org, retour de revue du 07/08/2026. if ( $page_slug === 'aiforya-hub' ) { wp_register_style( 'aiforya-hub-inline-bbf8d166', false, array(), ! empty( $plugin_args['version'] ) ? $plugin_args['version'] : '1.0.0' ); wp_enqueue_style( 'aiforya-hub-inline-bbf8d166' ); wp_add_inline_style( 'aiforya-hub-inline-bbf8d166', self::hub_css() ); } } /** * Build the AIFORYA hub menu and all sub-pages. * Called at admin_menu priority 99. */ public static function build_menu(): void { if ( self::$menu_created ) { return; } self::$menu_created = true; add_menu_page( 'AIFORYA', 'AIFORYA', 'manage_options', 'aiforya-hub', [ self::class, 'render_hub_page' ], self::$icon_b64, 56 ); foreach ( self::$registered_plugins as $plugin ) { add_submenu_page( 'aiforya-hub', esc_html( $plugin['page_title'] ), esc_html( $plugin['menu_label'] ), esc_attr( $plugin['capability'] ), esc_attr( $plugin['slug'] ), is_callable( $plugin['callback'] ) ? $plugin['callback'] : '__return_empty_string' ); } } /** * Render the hub overview page. */ /** * CSS du hub AIFORYA. * * Il vivait dans une balise de style en dur au milieu de render_hub_page(), ce que * wordpress.org refuse (retour de revue du 07/08/2026, cite a la ligne pres). Il est * desormais injecte par wp_add_inline_style() depuis enqueue_plugin_assets(). * NOWDOC volontaire : ce bloc ne contient ni PHP ni variable, rien ne doit y etre * interpole. * * @return string */ private static function hub_css(): string { return <<<'AIFORYA_HUB_CSS' /* AIFORYA Hub — DARK MODE inline (modele 27/04 v35). Fond global noir, cards bleu-fonce, titre+description blancs, prix accent. */ body.toplevel_page_aiforya-hub #wpcontent, body.toplevel_page_aiforya-hub #wpbody, body.toplevel_page_aiforya-hub .wrap.aiforya-hub-wrap { background: #0a0a0f !important; color: #e5e7eb !important; } .aiforya-hub-wrap { max-width: none; padding: 16px 32px 32px; margin: 0 -22px -10px -22px; min-height: calc(100vh - 32px); background: #0a0a0f; } .aiforya-hub-header { display: grid; grid-template-columns: auto auto 1fr auto; grid-template-rows: auto auto; column-gap: 12px; row-gap: 2px; align-items: center; background: transparent; border: none; border-radius: 0; padding: 12px 0 24px; margin: 0 auto 20px; max-width: 1400px; } .aiforya-hub-header-inner { display: contents; } .aiforya-hub-header-inner > div { display: contents; } .aiforya-hub-logo { width: 260px; height: 88px; object-fit: contain; grid-column: 4; grid-row: 1 / span 2; justify-self: end; align-self: center; } .aiforya-hub-title { color: #ffffff !important; font-size: 28px !important; font-weight: 800 !important; line-height: 1.2 !important; letter-spacing: normal; margin: 0 !important; padding: 0 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; grid-column: 1; grid-row: 1; align-self: end; } .aiforya-hub-subtitle { color: #9ca3af; font-size: 14px; margin: 0; padding: 0; grid-column: 1 / span 3; grid-row: 2; align-self: start; } .aiforya-hub-badge { background: rgba(99,102,241,0.15); color: #c7d2fe; border: 1px solid rgba(99,102,241,0.35); border-radius: 999px; padding: 4px 10px; font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; white-space: nowrap; grid-column: 2; grid-row: 1; justify-self: start; align-self: end; margin: 0 0 6px 0; width: fit-content; } /* Layout 2 colonnes — extensions à gauche, panel actus+privacy à droite */ .aiforya-hub-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; max-width: 1400px; margin: 0 auto; align-items: start; } @media (max-width: 1100px) { .aiforya-hub-layout { grid-template-columns: 1fr; } } .aiforya-hub-main { min-width: 0; } .aiforya-hub-aside { display: flex; flex-direction: column; gap: 16px; position: sticky; top: 32px; } .aiforya-hub-panel { background: linear-gradient(180deg, #14142b 0%, #11112a 100%); border: 1px solid rgba(99,102,241,0.18); border-radius: 14px; padding: 18px 20px; color: #e5e7eb; } .aiforya-hub-panel__title { margin: 0 0 12px 0; font-size: 14px; font-weight: 700; color: #ffffff; letter-spacing: 0.01em; display: flex; align-items: center; gap: 8px; } .aiforya-hub-panel__icon { width: 16px; height: 16px; color: #c7d2fe; flex-shrink: 0; } .aiforya-hub-panel__lead { margin: 0 0 12px 0; color: #9ca3af; font-size: 12.5px; line-height: 1.5; } .aiforya-hub-news { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; } .aiforya-hub-news li { border-left: 2px solid rgba(99,102,241,0.4); padding-left: 12px; } .aiforya-hub-news__date { display: block; color: #6366f1; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; } .aiforya-hub-news strong { display: block; color: #ffffff; font-size: 13px; font-weight: 600; margin-bottom: 4px; line-height: 1.3; } .aiforya-hub-news p { margin: 0; color: #9ca3af; font-size: 12px; line-height: 1.5; } .aiforya-hub-panel__link { display: inline-block; margin-top: 14px; color: #c7d2fe; text-decoration: none; font-size: 12px; font-weight: 600; } .aiforya-hub-panel__link:hover { color: #ffffff; text-decoration: underline; } .aiforya-hub-privacy-checkbox { display: flex; align-items: flex-start; gap: 10px; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.22); border-radius: 8px; padding: 10px 12px; color: #e5e7eb; font-size: 12.5px; line-height: 1.5; cursor: pointer; margin: 0 0 12px 0; } .aiforya-hub-privacy-checkbox input[type="checkbox"] { margin: 2px 0 0 0; accent-color: #6366f1; } .aiforya-hub-panel__btn { background: #6366f1; color: #ffffff; border: none; border-radius: 8px; padding: 8px 14px; font-size: 12.5px; font-weight: 600; cursor: pointer; width: 100%; transition: background 0.15s ease; } .aiforya-hub-panel__btn:hover { background: #4f46e5; } .aiforya-hub-panel__legal { margin: 12px 0 0 0; font-size: 11px; color: #6b7280; line-height: 1.5; } .aiforya-hub-panel__legal a { color: #9ca3af; text-decoration: underline; } .aiforya-hub-panel__legal a:hover { color: #ffffff; } /* Grid extensions — adapté pour le nouveau layout 2 col */ .aiforya-hub-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; } @media (max-width: 1400px) { .aiforya-hub-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (max-width: 1100px) { .aiforya-hub-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (max-width: 800px) { .aiforya-hub-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 540px) { .aiforya-hub-grid { grid-template-columns: 1fr; } } .aiforya-hub-card { background: linear-gradient(180deg, #14142b 0%, #11112a 100%); border: 1px solid rgba(99,102,241,0.18); border-radius: 16px; overflow: hidden; box-shadow: 0 1px 0 rgba(255,255,255,0.03) inset, 0 12px 32px -16px rgba(0,0,0,0.6); transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; display: flex; flex-direction: column; color: inherit; text-decoration: none; } .aiforya-hub-card:hover, .aiforya-hub-card:focus { transform: translateY(-3px); border-color: rgba(99,102,241,0.4); box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset, 0 24px 48px -20px rgba(99,102,241,0.4); text-decoration: none; } .aiforya-hub-card-thumb-wrap { display: block; width: 100%; margin: 0; padding: 0; border: 0; line-height: 0; position: relative; aspect-ratio: 1 / 1; background: #0f0f23; overflow: hidden; } .aiforya-hub-card-thumb { width: 100%; height: 100%; aspect-ratio: 1/1; object-fit: cover; display: block; margin: 0; padding: 0; border: 0; } .aiforya-hub-card-thumb-placeholder { width: 100%; aspect-ratio: 1/1; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); } .aiforya-hub-card-thumb-placeholder span { color: rgba(255,255,255,0.5); font-size: 3rem; font-weight: 800; letter-spacing: -2px; } .aiforya-hub-card-body { padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 6px; background: transparent; border: none; } .aiforya-hub-card-category { display: inline-block; background: rgba(99,102,241,0.12); border: 1px solid rgba(99,102,241,0.25); border-radius: 999px; color: #c7d2fe; font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; padding: 4px 10px; margin: 0 0 8px 0; width: fit-content; } .aiforya-hub-card-name { color: #ffffff; font-size: 21px; font-weight: 700; line-height: 1.25; letter-spacing: -0.01em; margin: 0; padding: 0; background: transparent; } .aiforya-hub-card-description { color: #9ca3af; font-size: 13px; line-height: 1.5; margin: 6px 0 8px; padding: 0; background: transparent; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } .aiforya-hub-card-ver { color: #6b7280; font-size: 11px; font-weight: 500; display: block; margin: 0 0 4px; background: transparent; } .aiforya-hub-card-price { color: #ffffff; font-size: 14px; font-weight: 600; margin: 4px 0 6px; padding: 0; background: transparent; } .aiforya-hub-status { display: inline-flex; align-items: center; gap: 6px; background: rgba(16,185,129,0.12); color: #6ee7b7; border: 1px solid rgba(16,185,129,0.3); border-radius: 999px; padding: 4px 10px; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; width: fit-content; margin: 4px 0 0; } .aiforya-hub-status::before { content: ""; display: inline-block; width: 6px; height: 6px; background: #10b981; border-radius: 50%; box-shadow: 0 0 8px rgba(16,185,129,0.6); } .aiforya-hub-open { display: inline-flex; align-items: center; gap: 6px; background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); color: #fff !important; border-radius: 10px; padding: 9px 14px; font-size: 13px; font-weight: 600; text-decoration: none !important; transition: transform 0.15s ease, box-shadow 0.15s ease; width: fit-content; margin-top: 6px; box-shadow: 0 4px 12px -2px rgba(99,102,241,0.4); } .aiforya-hub-card:hover .aiforya-hub-open { transform: translateY(-1px); box-shadow: 0 8px 20px -4px rgba(99,102,241,0.5); } .aiforya-hub-empty { text-align: center; padding: 64px 32px; border: 1px dashed rgba(99,102,241,0.25); border-radius: 16px; background: rgba(99,102,241,0.04); color: #9ca3af; max-width: 600px; margin: 0 auto; } AIFORYA_HUB_CSS; } public static function render_hub_page(): void { $t = self::i18n(); $plugin_count = count( self::$registered_plugins ); $label = $plugin_count === 1 ? $t['plugins_label'] : $t['plugins_label_plural']; // Logo blanc haute résolution encodé en base64 (wordmark complet) $logo_b64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzQ1NiA1NzQiPjxnPjxnPjxwYXRoIGQ9Ik0xNjguMjU2LDQ2Ni4zNzhsLTg0LjA2OCwtMC41MzFjLTAsLTAgMzguMjI0LC00NC43NDQgOTMuNzkxLC0xMDEuODUzYzIyLjU2LC0yMS42OTcgMzIuMTA5LC0zNi45MzkgNDkuNDU5LC00NC4wMTFjNi4zNjMsLTIuNTk0IDE5LjQwNywtMy43NzUgMjkuMDExLC00LjUwNGwxMDguNjM5LDBjNDAuOTIyLDAgNzMuNTU4LC0yMS4zMiA5OS41MjcsLTUxLjIyNGMyMi4xNDUsLTI1LjUgNjIuOTcsLTc3LjI2NiA4MS4wODgsLTkzLjM3MWM5LjIxMSwtOC4xODcgMjcuNzYyLC0xNi44MTYgNDQuNjcsLTcuNzU5YzcuMzc3LDMuOTUyIDEzLjc1LDkuNzY2IDIwLjQ4NSwxNy4wMzJsMjguOTMyLDM2LjE1Yy0zMS43MywtMTguMTE4IC01NC4wNzQsNS4xNTggLTY2LjUwNiwxNy41OWMtMTkuOTg3LDE5Ljk4NyAtNjUuNTE0LDc3LjE1OSAtODYuMTkxLDEwMC41OTNjLTE2Ljc5MywxOS4wMzIgLTI5LjQ1OSwyOS41MjkgLTU5LjEyNywyOS41MjlsLTYwLjE0NCwtMGMtNTguNDU2LC0wIC03Ny41NTIsOS43MDYgLTEwMi4yNzcsMzguMjU3bC0yNS4xOTMsMjkuMDkxYy0xMi43OTgsMTIuNjI4IC0yNS4xNzUsMjEuODQ2IC0zNi4zNjIsMjcuODEyYy04LjAwMSw0LjI2NyAtMTQuNzgxLDcuMTk5IC0zNS43MzQsNy4xOTlabTk3LjE4MSwtMTg2LjgxNWM2My41NDgsLTY1LjI0NSAxMjIuNjY2LC0xMjUuODMzIDEzNi41NTgsLTEzOS43MjVjMzIuODA2LC0zMi44MDYgNDMuMDU4LC00Ni40NzUgODQuNzQ4LC00Ni40NzVsODAuNjQ4LDBjMCwwIC0xMDYuNjM1LDEyMS4xNTYgLTEyOS41OTksMTQ3LjU5OWMtMTkuMTQsMjIuMDQgLTQyLjU0MywzOC45OTIgLTcyLjcwNCwzOC45OTJsLTc3Ljc1NSwtMGMtMS4wMDYsLTAuMjA2IC0yLjA0MywtMC4zMjcgLTMuMTA1LC0wLjM1NWMtNi44MjYsLTAuMTgxIC0xMy4wNjIsLTAuMTkyIC0xOC43OTEsLTAuMDM2Wm00MDYuNzYzLC0yMy4xMjlsNjIuMDI3LDczLjQxYy0zOS42NDksLTExLjA1IC04NS44NTUsLTE4Ljk5NSAtMTAyLjc4NywtMC4xOTJjLTIzLjI1OCwyNS44MjcgLTU5LjY1NSw2NS44NjYgLTc2LjAyNSw4NS4xYy0yMi4wNywyNS45MzEgLTQzLjI5OSwzNy42MjMgLTU4LjU4Miw0My4zNDRjLTE1LjI3MSw1LjcxOCAtMjkuMTgyLDkuMSAtNDYuMDMzLDkuMWwtMTk0LjI1NCwtMC4yNmwxMC40NDEsLTEyLjA1N2M2LjI3NCwtNi4yNDUgMjkuOTYzLC0yOS4zNDcgMjkuOTYzLC0yOS4zNDdjMTguMDYyLC0yMC44NTcgMzIuNzE3LC0yNS45ODcgNzUuNDIxLC0yNS45ODdsMTEuNTk1LC0wYzQyLjc4LC0wIDYxLjU1MSwtMTQuMTA3IDg1Ljc2NiwtNDEuNTUxYzIwLjMzMSwtMjMuMDQxIDU3LjI5LC02OC43MTkgNzYuMTYzLC04OS4xMjJjMTYuNTY1LC0xNy45MSAzMC43NjQsLTI2Ljk0NiA0OC45MzYsLTI2LjI1YzkuNjc4LDAuMzcgMjIuODI0LDIuNTY1IDMzLjM2OSwxMy44MTJaIiBmaWxsPSIjMWQwNThiIiBzdHJva2U9IiMxZDA1OGIiIHN0cm9rZS13aWR0aD0iMXB4Ii8+PHBhdGggZD0iTTYxNC44ODgsMzQ3Ljc3MmM3Ljg2NywtOS40MiAyMy43MzIsLTI4LjA3OSAzNS4yNDYsLTM2LjA1OGM4Ljk0MSwtNi4xOTYgMjQuODcxLC0xMS40MTUgMzkuODk3LC05LjA4N2MxMS42MzMsMS44MDIgMjUuODUzLDYuNTM3IDM4Ljc3NSwyMC41NzJsOTEuMTI2LDExMC41MjVsLTQxLjI5NSwwLjA1OWMtMC45MDksLTAuMTQ0IC0xLjg0MiwtMC4yMTkgLTIuNzkyLC0wLjIxOWMtNzQuMzM4LDAgLTkxLjI3MywwLjUzMyAtMTE4Ljg0OCwtMzMuMTMzYy0xMS4xNTQsLTEzLjYxOCAtMjkuNTQ3LC0zNC44NzcgLTQyLjEwOSwtNTIuNjU5WiIgZmlsbD0iIzFkMDU4YiIgc3Ryb2tlPSIjMWQwNThiIiBzdHJva2Utd2lkdGg9IjFweCIvPjwvZz48cGF0aCBkPSJNMTY4LjI1Niw0NjQuMjg5bC04NC4wNjgsLTAuNTMxYy0wLC0wIDM4LjIyNCwtNDQuNzQ1IDkzLjc5MSwtMTAxLjg1M2MyMi41NiwtMjEuNjk3IDMyLjEwOSwtMzYuOTM5IDQ5LjQ1OSwtNDQuMDExYzYuMzYzLC0yLjU5NCAxOS40MDcsLTMuNzc1IDI5LjAxMSwtNC41MDRsMTA4LjYzOSwwYzQwLjkyMiwwIDczLjU1OCwtMjEuMzIgOTkuNTI3LC01MS4yMjRjMjIuMTQ1LC0yNS41IDYyLjk3LC03Ny4yNjYgODEuMDg4LC05My4zNzFjOS4yMTEsLTguMTg4IDI3Ljc2MiwtMTYuODE2IDQ0LjY3LC03Ljc1OWM3LjM3NywzLjk1MiAxMy43NSw5Ljc2NiAyMC40ODUsMTcuMDMybDI4LjkzMiwzNi4xNDljLTMxLjczLC0xOC4xMTcgLTU0LjA3NCw1LjE1OSAtNjYuNTA2LDE3LjU5MWMtMTkuOTg3LDE5Ljk4NyAtNjUuNTE0LDc3LjE1OSAtODYuMTkxLDEwMC41OTNjLTE2Ljc5MywxOS4wMzIgLTI5LjQ1OSwyOS41MjkgLTU5LjEyNywyOS41MjlsLTYwLjE0NCwtMGMtNTguNDU2LC0wIC03Ny41NTIsOS43MDYgLTEwMi4yNzcsMzguMjU3bC0yNS4xOTMsMjkuMDkxYy0xMi43OTgsMTIuNjI3IC0yNS4xNzUsMjEuODQ2IC0zNi4zNjIsMjcuODEyYy04LjAwMSw0LjI2NyAtMTQuNzgxLDcuMTk5IC0zNS43MzQsNy4xOTlabTk3LjE4MSwtMTg2LjgxNmM2My41NDgsLTY1LjI0NCAxMjIuNjY2LC0xMjUuODMyIDEzNi41NTgsLTEzOS43MjRjMzIuODA2LC0zMi44MDYgNDMuMDU4LC00Ni40NzUgODQuNzQ4LC00Ni40NzVsODAuNjQ4LC0wYzAsLTAgLTEwNi42MzUsMTIxLjE1NiAtMTI5LjU5OSwxNDcuNTk4Yy0xOS4xNCwyMi4wNDEgLTQyLjU0MywzOC45OTMgLTcyLjcwNCwzOC45OTNsLTc3Ljc1NSwtMGMtMS4wMDYsLTAuMjA2IC0yLjA0MywtMC4zMjcgLTMuMTA1LC0wLjM1NWMtNi44MjYsLTAuMTgyIC0xMy4wNjIsLTAuMTkyIC0xOC43OTEsLTAuMDM3Wm00MDYuNzYzLC0yMy4xMjhsNjIuMDI3LDczLjQxYy0zOS42NDksLTExLjA1MSAtODUuODU1LC0xOC45OTUgLTEwMi43ODcsLTAuMTkzYy0yMy4yNTgsMjUuODI4IC01OS42NTUsNjUuODY3IC03Ni4wMjUsODUuMTAxYy0yMi4wNywyNS45MzEgLTQzLjI5OSwzNy42MjIgLTU4LjU4Miw0My4zNDRjLTE1LjI3MSw1LjcxOCAtMjkuMTgyLDkuMDk5IC00Ni4wMzMsOS4wOTlsLTE5NC4yNTQsLTAuMjU5bDEwLjQ0MSwtMTIuMDU3YzYuMjc0LC02LjI0NiAyOS45NjMsLTI5LjM0NyAyOS45NjMsLTI5LjM0N2MxOC4wNjIsLTIwLjg1NyAzMi43MTcsLTI1Ljk4OCA3NS40MjEsLTI1Ljk4OGw1NS41OTUsMGM0Mi43OCwwIDYxLjU1MSwtMTQuMTA2IDg1Ljc2NiwtNDEuNTVjMjAuMzMxLC0yMy4wNDIgNTcuMjksLTY4LjcxOSA3Ni4xNjMsLTg5LjEyM2MxNi41NjUsLTE3LjkwOSAzMC43NjQsLTI2Ljk0NSA0OC45MzYsLTI2LjI0OWM5LjY3OCwwLjM3IDIyLjgyNCwyLjU2NSAzMy4zNjksMTMuODEyWiIgZmlsbD0idXJsKCNMMSkiLz48cGF0aCBkPSJNNjE0Ljg4OCwzNDUuNjgzYzcuODY3LC05LjQyIDIzLjczMiwtMjguMDggMzUuMjQ2LC0zNi4wNThjOC45NDEsLTYuMTk3IDI0Ljg3MSwtMTEuNDE1IDM5Ljg5NywtOS4wODdjMTEuNjMzLDEuODAyIDI1Ljg1Myw2LjUzNyAzOC43NzUsMjAuNTcybDkxLjEyNiwxMTAuNTI1bC00MS4yOTUsMC4wNTljLTAuOTA5LC0wLjE0NCAtMS44NDIsLTAuMjE5IC0yLjc5MiwtMC4yMTljLTc0LjMzOCwwIC05MS4yNzMsMC41MzMgLTExOC44NDgsLTMzLjEzM2MtMTEuMTU0LC0xMy42MTggLTI5LjU0NywtMzQuODc3IC00Mi4xMDksLTUyLjY1OVoiIGZpbGw9InVybCgjTDIpIiBzdHJva2U9IiM0ZjJlZWEiIHN0cm9rZS13aWR0aD0iMXB4Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0xMjAxLjczLDM4NS4wMzZsLTEzNi44MTQsMGwtMjIuNzE2LDQ4LjMxOGwtOTIuOTMsMGwxMzMuNzE2LC0yNjIuOTQxbDEwMS43MDcsMGwxMzMuNzE2LDI2Mi45NDFsLTkzLjk2MiwwbC0yMi43MTcsLTQ4LjMxOFptLTIyLjcxNiwtNDguNjkzbC00NS40MzIsLTk3LjM4NWwtNDUuOTQ5LDk3LjM4NWw5MS4zODEsMFoiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxyZWN0IHg9IjEzNTIuNDgiIHk9IjE2OC41NDEiIHdpZHRoPSI4OC4yODQiIGhlaWdodD0iMjY0LjgxNCIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJub256ZXJvIi8+PGc+PHBhdGggZD0iTTE4NjIuNzYsMTY4LjU0MWwwLDUwLjk0bC0xNDkuNzIxLC0wbDAsNTguMDU2bDExNS42NDcsMGwtMCw0OC42OTNsLTExNS42NDcsMGwwLDEwNy4xMjRsLTg4LjI4MywwbC0wLC0yNjQuODEzbDIzOC4wMDQsLTBaIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxNC44MXB4IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLW1pdGVybGltaXQ9IjIiLz48cGF0aCBkPSJNMjA4MS42NiwxNjQuNDJjMzQuNDE4LDAgNjUuNjUzLDUuODA2IDkzLjcwNCwxNy40MTdjMjguMDUxLDExLjYxMiA1MC4xNjUsMjcuNzE4IDY2LjM0Miw0OC4zMTljMTYuMTc3LDIwLjYgMjQuMjY1LDQzLjg4NiAyNC4yNjUsNjkuODU1YzAsMjUuOTcgLTguMTc0LDQ5LjMxNyAtMjQuNTIzLDcwLjA0M2MtMTYuMzQ5LDIwLjcyNSAtMzguNTQ5LDM2Ljg5NCAtNjYuNiw0OC41MDVjLTI4LjA1MSwxMS42MTIgLTU5LjExNCwxNy40MTcgLTkzLjE4OCwxNy40MTdjLTM0LjA3NSwwIC02NS4xMzgsLTUuODA1IC05My4xODksLTE3LjQxN2MtMjguMDUxLC0xMS42MTEgLTUwLjI1MSwtMjcuNzggLTY2LjYsLTQ4LjUwNWMtMTYuMzQ5LC0yMC43MjYgLTI0LjUyMywtNDQuMDczIC0yNC41MjMsLTcwLjA0M2MwLC0yNS45NjkgOC4xNzQsLTQ5LjI1NSAyNC41MjMsLTY5Ljg1NWMxNi4zNDksLTIwLjYwMSAzOC41NDksLTM2LjcwNyA2Ni42LC00OC4zMTljMjguMDUxLC0xMS42MTEgNTkuMTE0LC0xNy40MTcgOTMuMTg5LC0xNy40MTdabS0wLDU5LjU1NWMtMjguOTEyLDAgLTUxLjg4Niw2LjgwNSAtNjguOTI0LDIwLjQxNGMtMTcuMDM3LDEzLjYwOSAtMjUuNTU1LDMyLjE1IC0yNS41NTUsNTUuNjIyYy0wLDIzLjIyMyA4LjUxOCw0MS43MDEgMjUuNTU1LDU1LjQzNWMxNy4wMzgsMTMuNzM0IDQwLjAxMiwyMC42MDEgNjguOTI0LDIwLjYwMWMyOC41NjcsLTAgNTEuNDU1LC02Ljg2NyA2OC42NjUsLTIwLjYwMWMxNy4yMDksLTEzLjczNCAyNS44MTQsLTMyLjIxMiAyNS44MTQsLTU1LjQzNWMtMCwtMjMuMjIzIC04LjUxOSwtNDEuNzAxIC0yNS41NTYsLTU1LjQzNWMtMTcuMDM3LC0xMy43MzQgLTQwLjAxMiwtMjAuNjAxIC02OC45MjMsLTIwLjYwMVoiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjE0LjgxcHgiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIvPjxwYXRoIGQ9Ik0yNTAyLjk1LDQzMy4zNTRsLTc4Ljk5MSwtMTAwLjAwN2wtMjAuMTM0LC0wbC0wLDEwMC4wMDdsLTg4LjI4NCwwbC0wLC0yNjQuODEzbDE1MC43NTMsLTBjMjguOTEyLC0wIDUzLjM0OSwzLjYyIDczLjMxMiwxMC44NjJjMTkuOTYzLDcuMjQxIDM0LjkzNSwxNy4xNjcgNDQuOTE2LDI5Ljc3N2M5Ljk4MiwxMi42MSAxNC45NzIsMjcuMDMxIDE0Ljk3Miw0My4yNjJjMCwxOS4yMjcgLTcuMzE0LDM1LjU4MyAtMjEuOTQyLDQ5LjA2N2MtMTQuNjI4LDEzLjQ4NCAtMzUuODgxLDIyLjcyNCAtNjMuNzYsMjcuNzE4bDg2LjczNSwxMDQuMTI3bC05Ny41NzcsMFptLTk5LjEyNSwtMTQzLjgzMWw1NC43MjUsMGMxNy41NTQsMCAzMC40NjEsLTIuODA5IDM4LjcyMSwtOC40MjdjOC4yNiwtNS42MTkgMTIuMzkxLC0xMy45MjEgMTIuMzkxLC0yNC45MDhjLTAsLTEwLjQ4OCAtNC4yMTcsLTE4LjcyOCAtMTIuNjQ5LC0yNC43MjFjLTguNDMzLC01Ljk5MyAtMjEuMjU0LC04Ljk5IC0zOC40NjMsLTguOTlsLTU0LjcyNSwwbC0wLDY3LjA0NloiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjE0LjgxcHgiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIvPjwvZz48cGF0aCBkPSJNMzA0Mi43OCwxNjguNTQxbC0xMjYuNDg5LDE3Ni43OTJsMCw4OC4wMjFsLTg4LjgsMGwwLC04OC4wMjFsLTEyNi40ODgsLTE3Ni43OTJsMTAxLjE5MSwtMGw3MC4yMTQsMTA3Ljg3M2w3MC4yMTQsLTEwNy44NzNsMTAwLjE1OCwtMFoiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxwYXRoIGQ9Ik0zMjQzLjUzLDM4NS4wMzZsLTEzNi44MTQsMGwtMjIuNzE2LDQ4LjMxOGwtOTIuOTMsMGwxMzMuNzE2LC0yNjIuOTQxbDEwMS43MDcsMGwxMzMuNzE2LDI2Mi45NDFsLTkzLjk2MywwbC0yMi43MTYsLTQ4LjMxOFptLTIyLjcxNiwtNDguNjkzbC00NS40MzMsLTk3LjM4NWwtNDUuOTQ5LDk3LjM4NWw5MS4zODIsMFoiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvZz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9IkwxIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMTU0LjUzNCwyMTcuODUyLC0xMjUuMjg1LDg4Ljg3MTIsNDQ5LjgzNyw4Mi41NTA5KSI+PHN0b3Agb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojMDBiYWYyO3N0b3Atb3BhY2l0eToxIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdHlsZT0ic3RvcC1jb2xvcjojNTUzMGZmO3N0b3Atb3BhY2l0eToxIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkwyIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoLTk5LjA5MzUsLTg3LjE0MDYsNTUuOTc5NCwtNjMuNjU4LDc2MS44MzEsNDA0LjI3NykiPjxzdG9wIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6IzRmMmVlYTtzdG9wLW9wYWNpdHk6MSIvPjxzdG9wIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6IzI3MGVhMTtzdG9wLW9wYWNpdHk6MSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjwvc3ZnPg=='; ?>