array( 'below_favourite' => __('Below favourite (right column)', 'ai-eshop-optimizer'), 'top_left' => __('Top-left over image', 'ai-eshop-optimizer'), 'top_right' => __('Top-right over image', 'ai-eshop-optimizer'), 'inline' => __('Inline above title', 'ai-eshop-optimizer'), ), 'pdp' => array( 'top_left' => __('Top-left over gallery', 'ai-eshop-optimizer'), 'top_right' => __('Top-right over gallery', 'ai-eshop-optimizer'), 'inline' => __('Inline above title (right column)', 'ai-eshop-optimizer'), ), 'recos' => array( 'inline' => __('Inline above title', 'ai-eshop-optimizer'), 'top_left' => __('Top-left over image', 'ai-eshop-optimizer'), 'top_right' => __('Top-right over image', 'ai-eshop-optimizer'), ), 'side_cart' => array( 'inline' => __('Inline above title', 'ai-eshop-optimizer'), 'top_left' => __('Top-left over thumb', 'ai-eshop-optimizer'), ), ); } public static function shape_options() { return array( 'chip' => __('Chip (rounded rectangle)', 'ai-eshop-optimizer'), 'circle' => __('Circle (round, icon-only friendly)', 'ai-eshop-optimizer'), ); } public static function default_location($surface) { $opts = self::location_options(); if (!isset($opts[$surface]) || empty($opts[$surface])) { return ''; } return (string) array_key_first($opts[$surface]); } public static function default_shape() { $opts = self::shape_options(); return $opts ? (string) array_key_first($opts) : 'chip'; } public static function migrate_legacy_placement($legacy) { $legacy = (string) $legacy; if ($legacy === '') { return null; } if ($legacy === 'below_favourite') { return array('location' => 'below_favourite', 'shape' => 'circle'); } if (strpos($legacy, 'top_left_') === 0) { return array('location' => 'top_left', 'shape' => 'chip'); } if (strpos($legacy, 'top_right_') === 0) { return array('location' => 'top_right', 'shape' => 'chip'); } if (strpos($legacy, 'inline') === 0) { return array('location' => 'inline', 'shape' => 'chip'); } return null; } public static function defaults() { return array( 'version' => self::SETTINGS_VERSION, 'hide_wc_onsale' => true, 'sizes' => array( 'archive' => array('desktop' => 11, 'mobile' => 10), 'pdp' => array('desktop' => 13, 'mobile' => 11), 'recos' => array('desktop' => 10, 'mobile' => 9), 'side_cart' => array('desktop' => 10, 'mobile' => 9), ), 'placements' => array( 'archive' => array('location' => 'below_favourite', 'shape' => 'circle'), 'pdp' => array('location' => 'top_left', 'shape' => 'chip'), 'recos' => array('location' => 'inline', 'shape' => 'chip'), 'side_cart' => array('location' => 'top_left', 'shape' => 'chip'), ), 'families' => array( 'gift' => self::default_family('', '#0a7d3a', '#ffffff', 5, '🎁'), 'monthly_offer' => self::default_family('', '#F46767', '#ffffff', 10), 'sale' => self::default_family('', '#dc2626', '#ffffff', 15), 'pro' => self::default_family('PRO', '#1f3b8b', '#ffffff', 20, 'β˜…'), 'new' => self::default_family('', '#1a1a1a', '#ffffff', 30), ), ); } private static function i18n_family_labels() { return array( 'sale' => __('SALE', 'ai-eshop-optimizer'), 'monthly_offer' => __('MONTHLY OFFER', 'ai-eshop-optimizer'), 'new' => __('NEW', 'ai-eshop-optimizer'), 'gift' => __('+ Free gift', 'ai-eshop-optimizer'), ); } private static function fill_i18n_defaults(array $out) { if (!did_action('init')) { return $out; } if (!isset($out['families']) || !is_array($out['families'])) { return $out; } foreach (self::i18n_family_labels() as $key => $label) { if (!isset($out['families'][$key]) || !is_array($out['families'][$key])) { continue; } if ('' === trim((string) ($out['families'][$key]['label'] ?? ''))) { $out['families'][$key]['label'] = $label; } } return $out; } private static function default_family($label, $bg, $fg, $order, $icon = '') { return array( 'enabled' => true, 'label' => (string) $label, 'icon' => (string) $icon, 'icon_image' => 0, 'icon_image_url' => '', 'order' => (int) $order, 'bg' => (string) $bg, 'fg' => (string) $fg, 'surfaces' => array( 'archive' => true, 'pdp' => true, 'recos' => true, 'side_cart' => false, ), ); } public static function get_settings($raw = false) { $stored = get_option(self::OPTION_KEY, array()); if (!is_array($stored)) { $stored = array(); } $defaults = self::defaults(); $out = array_merge($defaults, $stored); $out['sizes'] = array(); foreach ($defaults['sizes'] as $surface => $def) { $stored_val = isset($stored['sizes'][$surface]) ? $stored['sizes'][$surface] : null; if (is_int($stored_val) || (is_string($stored_val) && ctype_digit($stored_val))) { $out['sizes'][$surface] = array( 'desktop' => (int) $stored_val, 'mobile' => max(8, (int) round((int) $stored_val * 0.9)), ); } elseif (is_array($stored_val)) { $out['sizes'][$surface] = array( 'desktop' => isset($stored_val['desktop']) ? (int) $stored_val['desktop'] : (int) $def['desktop'], 'mobile' => isset($stored_val['mobile']) ? (int) $stored_val['mobile'] : (int) $def['mobile'], ); } else { $out['sizes'][$surface] = $def; } } $out['placements'] = $defaults['placements']; $stored_placements = isset($stored['placements']) && is_array($stored['placements']) ? $stored['placements'] : array(); foreach ($defaults['placements'] as $surface => $def_pl) { $sp = isset($stored_placements[$surface]) ? $stored_placements[$surface] : null; if (is_string($sp)) { $migrated = self::migrate_legacy_placement($sp); $out['placements'][$surface] = $migrated ? $migrated : $def_pl; } elseif (is_array($sp)) { $out['placements'][$surface] = array( 'location' => isset($sp['location']) ? (string) $sp['location'] : $def_pl['location'], 'shape' => isset($sp['shape']) ? (string) $sp['shape'] : $def_pl['shape'], ); } } if (!isset($stored_placements['archive']) && isset($stored['archive_layout']) && is_string($stored['archive_layout'])) { $migrated = self::migrate_legacy_placement($stored['archive_layout']); if ($migrated) { $out['placements']['archive'] = $migrated; } } $valid_locations = self::location_options(); $valid_shapes = self::shape_options(); foreach ($out['placements'] as $surface => $pl) { if (!isset($valid_locations[$surface][$pl['location']])) { $out['placements'][$surface]['location'] = self::default_location($surface); } if (!isset($valid_shapes[$pl['shape']])) { $out['placements'][$surface]['shape'] = self::default_shape(); } } $out['families'] = isset($stored['families']) && is_array($stored['families']) ? $stored['families'] : array(); foreach ($defaults['families'] as $key => $family_default) { $stored_family = isset($out['families'][$key]) && is_array($out['families'][$key]) ? $out['families'][$key] : array(); $merged = array_merge($family_default, $stored_family); $merged['surfaces'] = array_merge( $family_default['surfaces'], is_array($stored_family['surfaces'] ?? null) ? $stored_family['surfaces'] : array() ); $out['families'][$key] = $merged; } return $raw ? $out : self::fill_i18n_defaults($out); } public static function get_family($key) { $s = self::get_settings(); return isset($s['families'][$key]) ? $s['families'][$key] : null; } public static function family_enabled_for_surface($key, $surface) { $f = self::get_family($key); if (!$f || empty($f['enabled'])) { return false; } return !empty($f['surfaces'][$surface]); } public static function maybe_migrate() { $existing = get_option(self::OPTION_KEY, null); if (is_array($existing) && isset($existing['version']) && (int) $existing['version'] >= self::SETTINGS_VERSION) { return; } $unified = self::build_from_legacy(); update_option(self::OPTION_KEY, $unified, false); } public static function maybe_migrate_order() { if (get_option('aieo_badges_order_v2')) { return; } $stored = get_option(self::OPTION_KEY, null); if (!is_array($stored) || empty($stored['families']) || !is_array($stored['families'])) { update_option('aieo_badges_order_v2', 1, false); return; } $order = static function ($key) use ($stored) { return isset($stored['families'][$key]['order']) ? (int) $stored['families'][$key]['order'] : null; }; $untouched = 10 === $order('sale') && 15 === $order('monthly_offer') && 20 === $order('pro') && 30 === $order('new') && in_array($order('gift'), array(40, 5), true); if ($untouched) { $defaults = self::defaults(); foreach ($defaults['families'] as $key => $family) { if (isset($stored['families'][$key]) && is_array($stored['families'][$key])) { $stored['families'][$key]['order'] = (int) $family['order']; } } update_option(self::OPTION_KEY, $stored, false); } update_option('aieo_badges_order_v2', 1, false); } public static function build_from_legacy() { $unified = self::defaults(); $pls = get_option('aieo_dmm_product_labels_settings', array()); if (!is_array($pls)) { $pls = array(); } $legacy_pls = get_option('om_ppe_product-labels', array()); if (!is_array($legacy_pls)) { $legacy_pls = array(); } $pls = array_merge($legacy_pls, $pls); if (!empty($pls)) { $rule = isset($pls['display_rules']) ? $pls['display_rules'] : 'products_on_sale'; $family_key = ($rule === 'new_products') ? 'new' : 'sale'; $unified['families'][$family_key]['enabled'] = !empty($pls['enabled']); if (!empty($pls['label_text'])) { $unified['families'][$family_key]['label'] = (string) $pls['label_text']; } if (!empty($pls['background_color'])) { $unified['families'][$family_key]['bg'] = (string) $pls['background_color']; } if (!empty($pls['text_color'])) { $unified['families'][$family_key]['fg'] = (string) $pls['text_color']; } } if (class_exists('AIEO_DMM_Settings')) { $ml = (string) AIEO_DMM_Settings::get('monthly_offer_label', ''); if ($ml !== '') { $unified['families']['monthly_offer']['label'] = $ml; } $nl = (string) AIEO_DMM_Settings::get('new_product_label', ''); if ($nl !== '') { $unified['families']['new']['label'] = $nl; } } $rp = get_option('aieo_dmm_role_pricing_settings', array()); if (is_array($rp)) { if (!empty($rp['pro_badge_label'])) { $unified['families']['pro']['label'] = (string) $rp['pro_badge_label']; } if (!empty($rp['pro_badge_bg_color'])) { $unified['families']['pro']['bg'] = (string) $rp['pro_badge_bg_color']; } if (!empty($rp['pro_badge_text_color'])) { $unified['families']['pro']['fg'] = (string) $rp['pro_badge_text_color']; } if (!empty($rp['pro_badge_tooltip'])) { $unified['families']['pro']['tooltip'] = (string) $rp['pro_badge_tooltip']; } } $arch_pos = class_exists('AIEO_DMM_Settings') ? (string) AIEO_DMM_Settings::get('archive_badge_position', 'image_top_left') : 'image_top_left'; $arch_placement = ($arch_pos === 'below_favourite') ? 'below_favourite' : 'top_left_over_image'; $unified['placements']['archive'] = $arch_placement; return $unified; } public static function sanitize(array $input) { $defaults = self::defaults(); $out = $defaults; $out['version'] = self::SETTINGS_VERSION; $out['hide_wc_onsale'] = !empty($input['hide_wc_onsale']); $valid_families = array_keys($defaults['families']); $valid_surfaces = array('archive', 'pdp', 'recos', 'side_cart'); $location_opts = self::location_options(); $shape_opts = self::shape_options(); $out['sizes'] = array(); foreach ($valid_surfaces as $s) { $def_d = (int) $defaults['sizes'][$s]['desktop']; $def_m = (int) $defaults['sizes'][$s]['mobile']; $raw = isset($input['sizes'][$s]) ? $input['sizes'][$s] : array(); if (is_int($raw) || (is_string($raw) && ctype_digit($raw))) { $d = max(8, min(24, (int) $raw)); $m = max(8, min(24, (int) round($d * 0.9))); } else { $d = isset($raw['desktop']) ? max(8, min(24, (int) $raw['desktop'])) : $def_d; $m = isset($raw['mobile']) ? max(8, min(24, (int) $raw['mobile'])) : $def_m; } $out['sizes'][$s] = array('desktop' => $d, 'mobile' => $m); } $out['placements'] = array(); foreach ($valid_surfaces as $s) { $raw_loc = isset($input['placements'][$s]['location']) ? (string) $input['placements'][$s]['location'] : ''; $raw_shape = isset($input['placements'][$s]['shape']) ? (string) $input['placements'][$s]['shape'] : ''; $out['placements'][$s] = array( 'location' => isset($location_opts[$s][$raw_loc]) ? $raw_loc : self::default_location($s), 'shape' => isset($shape_opts[$raw_shape]) ? $raw_shape : self::default_shape(), ); } $out['families'] = array(); foreach ($valid_families as $key) { $src = isset($input['families'][$key]) && is_array($input['families'][$key]) ? $input['families'][$key] : array(); $def = $defaults['families'][$key]; $img_id = isset($src['icon_image']) ? (int) $src['icon_image'] : 0; if ($img_id > 0) { $img_url = wp_get_attachment_url($img_id); $img_url = $img_url ? esc_url_raw($img_url) : ''; } else { $img_id = 0; $img_url = ''; } $f = array( 'enabled' => !empty($src['enabled']), 'label' => isset($src['label']) ? sanitize_text_field((string) $src['label']) : $def['label'], 'icon' => isset($src['icon']) ? sanitize_text_field((string) $src['icon']) : $def['icon'], 'icon_image' => $img_id, 'icon_image_url' => $img_url, 'order' => isset($src['order']) ? max(0, min(999, (int) $src['order'])) : $def['order'], 'bg' => isset($src['bg']) ? (sanitize_hex_color((string) $src['bg']) ?: $def['bg']) : $def['bg'], 'fg' => isset($src['fg']) ? (sanitize_hex_color((string) $src['fg']) ?: $def['fg']) : $def['fg'], ); $f['surfaces'] = array(); foreach ($valid_surfaces as $s) { $f['surfaces'][$s] = isset($src['surfaces'][$s]) ? !empty($src['surfaces'][$s]) : !empty($def['surfaces'][$s]); } if (isset($src['tooltip'])) { $f['tooltip'] = sanitize_text_field((string) $src['tooltip']); } $out['families'][$key] = $f; } return $out; } public function ajax_save() { AIEO_DMM::verify_ajax_request(); $raw = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : array(); if (!is_array($raw)) { $decoded = json_decode((string) $raw, true); $raw = is_array($decoded) ? $decoded : array(); } $clean = self::sanitize($raw); update_option(self::OPTION_KEY, $clean, false); if (array_key_exists('storefront', $raw) && class_exists('AIEO_DMM_Badge_Renderer')) { update_option(AIEO_DMM_Badge_Renderer::FLAG_OPTION, !empty($raw['storefront']) ? 1 : 0); } wp_send_json_success(array( 'message' => __('Badge settings saved.', 'ai-eshop-optimizer'), 'settings' => $clean, )); } public function render_subtab() { $s = self::get_settings(true); $i18n_labels = self::i18n_family_labels(); $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION); $ajax = admin_url('admin-ajax.php'); wp_enqueue_media(); $new_days = (int) AIEO_DMM_Settings::get('new_product_days', 60); $aieo_bd_new_form = 'aieo-card-badges-new-product-threshold-form'; list($aieo_bd_open, $aieo_bd_close) = AIEO_DMM_UI::card(array( 'anchor' => 'badges-new-product-threshold', 'icon' => 'calendar-days', 'title' => __('NEW badge validity in days', 'ai-eshop-optimizer'), 'description' => __('Number of days after publication during which a product counts as "new". Drives the NEW badge and the AI-chat recency boost. Set 0 to disable the NEW badge.', 'ai-eshop-optimizer'), 'footer' => class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::save_button($aieo_bd_new_form) : '', )); echo $aieo_bd_open; $aieo_bd_new_row = \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-badges-new-days', 'label' => __('Days since publication', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::input('new_product_days', (string) $new_days, array('id' => 'aieo-badges-new-days', 'type' => 'number', 'xs' => true, 'min' => 0, 'max' => 3650, 'step' => 1, 'unit' => __('days', 'ai-eshop-optimizer'))), )); echo AIEO_DMM_UI::card_form('aieo_dmm_save_badge_settings', 'badges_new_days', $aieo_bd_new_form, '
' . $aieo_bd_new_row . '
'); echo $aieo_bd_close; list($aieo_bd_open, $aieo_bd_close) = AIEO_DMM_UI::card(array( 'anchor' => 'badges-overview', 'title' => __('Badge settings', 'ai-eshop-optimizer'), 'class' => 'aieo-badges-form', 'footer' => '' . ' ' . \Rooster\Admin\States::outcome('', '', array('id' => 'aieo-badges-status')), )); echo $aieo_bd_open; $surface_labels = array( 'archive' => __('Archive (shop loop)', 'ai-eshop-optimizer'), 'pdp' => __('PDP (single product)', 'ai-eshop-optimizer'), 'recos' => __('Recos (recommendation rails)', 'ai-eshop-optimizer'), 'side_cart' => __('Side cart', 'ai-eshop-optimizer'), ); $shape_opts = self::shape_options(); $aieo_bp_rows = array(); foreach (self::location_options() as $sk => $loc_opts) { $aieo_bp_rows[] = array( 'sk' => (string) $sk, 'surface' => (string) $surface_labels[$sk], 'loc_opts' => $loc_opts, 'cur_loc' => isset($s['placements'][$sk]['location']) ? (string) $s['placements'][$sk]['location'] : self::default_location($sk), 'cur_shape' => isset($s['placements'][$sk]['shape']) ? (string) $s['placements'][$sk]['shape'] : self::default_shape(), ); } ob_start(); AIEO_Table::render(array( 'id' => 'aieo-badges-placements', 'page_id' => 'badges', 'row_key' => 'sk', 'sortable' => false, 'rows' => $aieo_bp_rows, 'columns' => array( array('key' => 'surface', 'label' => __('Surface', 'ai-eshop-optimizer'), 'type' => 'text', 'width' => '200px'), array( 'key' => 'cur_loc', 'label' => __('Location', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array('render' => static function ($r) { return \Rooster\Admin\Ui::select('', $r['loc_opts'], (string) $r['cur_loc'], array('id' => '', 'aria-label' => __('Location', 'ai-eshop-optimizer') . ', ' . $r['surface'], 'attrs' => array('data-placement-surface' => $r['sk'], 'data-placement-field' => 'location'))); }), ), array( 'key' => 'cur_shape', 'label' => __('Shape', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array('render' => static function ($r) use ($shape_opts) { return \Rooster\Admin\Ui::select('', $shape_opts, (string) $r['cur_shape'], array('id' => '', 'aria-label' => __('Shape', 'ai-eshop-optimizer') . ', ' . $r['surface'], 'attrs' => array('data-placement-surface' => $r['sk'], 'data-placement-field' => 'shape'))); }), ), ), )); $aieo_bd_matrix = (string) ob_get_clean(); $aieo_bd_placement = \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-badges-placement', 'label' => __('Placement per surface', 'ai-eshop-optimizer'), 'group' => true, 'control' => $aieo_bd_matrix, 'help' => __('Location is where the stack sits on each surface; Shape is the visual style of each chip. Combinations like "below favourite + chip" or "inline + circle" are supported. See the design-ownership doc for the CSS hooks.', 'ai-eshop-optimizer'), )); $aieo_bd_on = class_exists('AIEO_DMM_Badge_Renderer') && AIEO_DMM_Badge_Renderer::is_enabled(); $aieo_bd_storefront = \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-badges-storefront', 'label' => __('Show these badges in the store', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::toggle('badges_storefront', $aieo_bd_on, array('id' => 'aieo-badges-storefront', 'hidden' => false, 'label' => __('Draw the badges below on product cards, product pages and recommendations', 'ai-eshop-optimizer'))), 'help' => __('Off: WooCommerce\'s own sale badge shows, and the NEW, monthly offer, free gift and PRO badges of their own modules where those draw them.', 'ai-eshop-optimizer'), )); $aieo_bd_flash = \Rooster\Admin\Ui::field_row(array( 'id' => 'aieo-badges-hide-wc-onsale', 'label' => __('Hide WooCommerce native sale flash', 'ai-eshop-optimizer'), 'control' => \Rooster\Admin\Ui::toggle('hide_wc_onsale', !empty($s['hide_wc_onsale']), array('id' => 'aieo-badges-hide-wc-onsale', 'hidden' => false, 'label' => __('Replace WooCommerce\'s built-in .onsale badge with our SALE family', 'ai-eshop-optimizer'))), 'help' => __('Applies only while the badges show in the store, so a product on sale always has a sale badge.', 'ai-eshop-optimizer'), )); echo '
' . $aieo_bd_storefront . $aieo_bd_placement . $aieo_bd_flash . '
'; $aieo_bd_sizes = rooster_admin_card_section_html( __('Badge size per surface (px)', 'ai-eshop-optimizer'), array( 'id' => 'aieo-section-badges-sizes', 'description' => __('Mobile sizes apply at viewport width ≀ 600px; desktop sizes apply above. Padding/border-radius scale with the active size.', 'ai-eshop-optimizer'), ) ); echo $aieo_bd_sizes; $aieo_bd_size_rows = ''; foreach ($surface_labels as $sk => $sl) { $d = isset($s['sizes'][$sk]['desktop']) ? (int) $s['sizes'][$sk]['desktop'] : 11; $m = isset($s['sizes'][$sk]['mobile']) ? (int) $s['sizes'][$sk]['mobile'] : 10; $parts = array(); foreach (array('desktop' => array(__('Desktop', 'ai-eshop-optimizer'), $d), 'mobile' => array(__('Mobile (≀600px)', 'ai-eshop-optimizer'), $m)) as $bp => $p) { $pid = 'aieo-badges-size-' . $sk . '-' . $bp; $parts[] = array( 'label' => $p[0], 'id' => $pid, 'control' => \Rooster\Admin\Ui::input('', (string) $p[1], array('id' => $pid, 'type' => 'number', 'xs' => true, 'unit' => 'px', 'min' => 8, 'max' => 24, 'step' => 1, 'class' => 'aieo-badges-size', 'attrs' => array('data-size-surface' => $sk, 'data-size-bp' => $bp))), ); } $aieo_bd_size_rows .= \Rooster\Admin\Ui::field_row(array('id' => 'aieo-badges-size-' . $sk, 'label' => $sl, 'parts' => $parts)); } echo '
' . $aieo_bd_size_rows . '
'; echo rooster_admin_card_section_html( __('Badge families', 'ai-eshop-optimizer'), array('id' => 'aieo-card-badges-badge-families') ); $family_labels = array( 'sale' => __('SALE', 'ai-eshop-optimizer'), 'monthly_offer' => __('Monthly offer', 'ai-eshop-optimizer'), 'pro' => __('PRO (role price)', 'ai-eshop-optimizer'), 'new' => __('NEW', 'ai-eshop-optimizer'), 'gift' => __('Free gift', 'ai-eshop-optimizer'), ); $badge_rows = array(); foreach ($s['families'] as $key => $f) { $badge_rows[] = array( 'key' => (string) $key, 'name' => isset($family_labels[$key]) ? $family_labels[$key] : (string) $key, 'css_class' => '.aieo-badge--' . $key, 'enabled' => !empty($f['enabled']), 'icon' => (string) ($f['icon'] ?? ''), 'img_id' => (int) ($f['icon_image'] ?? 0), 'img_url' => (string) ($f['icon_image_url'] ?? ''), 'label' => (string) $f['label'], 'hint' => isset($i18n_labels[$key]) ? $i18n_labels[$key] : '', 'tooltip' => (string) ($f['tooltip'] ?? ''), 'bg' => (string) $f['bg'], 'fg' => (string) $f['fg'], 'order' => (int) $f['order'], 'surfaces' => isset($f['surfaces']) ? (array) $f['surfaces'] : array(), ); } $badge_input = static function ($class, $value, $column, $row, array $opts = array()) { return \Rooster\Admin\Ui::input('', (string) $value, $opts + array('id' => '', 'class' => $class, 'aria-label' => $column . ', ' . (string) ($row['name'] ?? ''))); }; AIEO_Table::render(array( 'class' => 'aieo-badges-families', 'columns' => array( array( 'key' => 'name', 'label' => __('Family', 'ai-eshop-optimizer'), 'type' => 'text', 'subline' => 'css_class', ), array( 'key' => 'enabled', 'label' => __('Enabled', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) { return ''; }, ), ), array( 'key' => 'icon', 'label' => __('Icon', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) use ($badge_input) { return $badge_input('aieo-badges-icon', $row['icon'], __('Icon', 'ai-eshop-optimizer'), $row, array('xs' => true, 'maxlength' => '8')); }, ), ), array( 'key' => 'img_url', 'label' => __('Image (overrides icon + label)', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) { $preview = ('' !== $row['img_url']) ? '' : 'βˆ…'; return '
' . '' . $preview . '' . '' . '' . \Rooster\Admin\Ui::button(__('Pick', 'ai-eshop-optimizer'), array('size' => 'sm', 'class' => 'aieo-badges-image-pick')) . \Rooster\Admin\Ui::button(__('Remove', 'ai-eshop-optimizer'), array('variant' => 'destructive', 'size' => 'sm', 'class' => 'aieo-badges-image-clear', 'attrs' => array('hidden' => empty($row['img_id'])))) . '
'; }, ), ), array( 'key' => 'label', 'label' => __('Label', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) use ($badge_input) { return $badge_input('aieo-badges-label', $row['label'], __('Label', 'ai-eshop-optimizer'), $row, array('medium' => true, 'placeholder' => (string) $row['hint'])); }, ), ), array( 'key' => 'tooltip', 'label' => __('Tooltip', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) use ($badge_input) { return $badge_input('aieo-badges-tooltip', $row['tooltip'], __('Tooltip', 'ai-eshop-optimizer'), $row, array('medium' => true)); }, ), ), array( 'key' => 'bg', 'label' => __('BG', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) { return \Rooster\Admin\Fields::color('badge_bg', (string) $row['bg'], array('id' => 'aieo-badges-bg-' . $row['key'], 'aria-label' => __('BG', 'ai-eshop-optimizer'))); }, ), ), array( 'key' => 'fg', 'label' => __('FG', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) { return \Rooster\Admin\Fields::color('badge_fg', (string) $row['fg'], array('id' => 'aieo-badges-fg-' . $row['key'], 'aria-label' => __('FG', 'ai-eshop-optimizer'))); }, ), ), array( 'key' => 'order', 'label' => __('Order', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) use ($badge_input) { return $badge_input('aieo-badges-order', (string) (int) $row['order'], __('Order', 'ai-eshop-optimizer'), $row, array('type' => 'number', 'xs' => true)); }, ), ), array( 'key' => 'surfaces', 'label' => __('Surfaces', 'ai-eshop-optimizer'), 'type' => 'control', 'format' => array( 'render' => static function ($row) { $out = ''; foreach (array('archive', 'pdp', 'recos', 'side_cart') as $surface) { $out .= ''; } return $out; }, ), ), ), 'rows' => $badge_rows, 'row_key' => 'key', 'row_data' => array('family' => 'key'), 'page_id' => 'badges', )); $aieo_bd_css = "/* Container β€” surface targeting */\n" . ".aieo-badge-stack { /* all surfaces */ }\n" . ".aieo-badge-stack[data-surface=\"archive\"] { /* shop loop */ }\n" . ".aieo-badge-stack[data-surface=\"pdp\"] { /* single product */ }\n" . ".aieo-badge-stack[data-surface=\"recos\"] { /* recommendation rails */ }\n" . ".aieo-badge-stack[data-surface=\"side_cart\"] { /* side-cart cards */ }\n" . "\n/* Individual badges */\n" . ".aieo-badge { /* base styling for every badge */ }\n" . ".aieo-badge--sale, .aieo-badge--monthly_offer,\n" . ".aieo-badge--pro, .aieo-badge--new, .aieo-badge--gift { /* per-family */ }\n" . ".aieo-badge__icon { /* the icon span */ }\n" . ".aieo-badge__label { /* the label span */ }\n"; echo \Rooster\Admin\Ui::fold( __('CSS classes for theme customisation', 'ai-eshop-optimizer'), \Rooster\Admin\Guidance::text(__('Each badge emits these classes β€” drop overrides into your theme stylesheet to fine-tune fonts, sizing, shadows, etc.', 'ai-eshop-optimizer')) . '
' . esc_html($aieo_bd_css) . '
' ); echo $aieo_bd_close; ?>