Analytics
2 weeks ago
Database
2 months ago
DynamicFieldResolver.php
1 month ago
Elementor_Enhancer.php
6 months ago
EmbedPress_Core_Installer.php
6 years ago
EmbedPress_Notice.php
3 months ago
EmbedPress_Plugin_Usage_Tracker.php
1 month ago
Extend_CustomPlayer_Controls.php
2 months ago
Extend_Elementor_Controls.php
1 year ago
FeatureNoticeManager.php
3 days ago
FeatureNotices.php
3 days ago
FeaturePreviewModal.php
3 days ago
Feature_Enhancer.php
1 month ago
GoogleReviewsAdminPage.php
3 days ago
GoogleReviewsApify.php
3 days ago
GoogleReviewsManaged.php
3 days ago
GoogleReviewsRenderer.php
3 days ago
GoogleReviewsRestController.php
3 days ago
GoogleReviewsStore.php
3 days ago
Helper.php
3 days ago
Pdf_Thumbnail_Handler.php
2 months ago
PermalinkHelper.php
10 months ago
View_Count_Display.php
2 weeks ago
GoogleReviewsAdminPage.php
408 lines
| 1 | <?php |
| 2 | |
| 3 | namespace EmbedPress\Includes\Classes; |
| 4 | |
| 5 | (defined('ABSPATH') && defined('EMBEDPRESS_IS_LOADED')) or die("No direct script access allowed."); |
| 6 | |
| 7 | /** |
| 8 | * Standalone admin page for Google Reviews: API key, cache TTL, searchable |
| 9 | * place picker, and shortcode generator with live preview. All values are |
| 10 | * persisted via the REST controller; no Vite build required. |
| 11 | */ |
| 12 | class GoogleReviewsAdminPage |
| 13 | { |
| 14 | const PAGE_SLUG = 'embedpress-google-reviews'; |
| 15 | |
| 16 | public static function register() |
| 17 | { |
| 18 | add_action('admin_menu', [__CLASS__, 'add_menu'], 30); |
| 19 | // Menu ordering (QA #4) is owned by EmbedpressSettings::reorder_submenu_items |
| 20 | // (admin_menu priority 999) — "Google Reviews" is placed right after |
| 21 | // Analytics in its canonical priority map. Don't add a second reorder |
| 22 | // here; it would just be overwritten by that later-running one. |
| 23 | |
| 24 | // Keep this dedicated settings screen clean — third-party plugins hook |
| 25 | // admin_notices globally, so suppress them here (mirrors what the main |
| 26 | // EmbedPress settings/onboarding pages already do). Runs late so it |
| 27 | // wins over notices registered earlier. |
| 28 | add_action('in_admin_header', function () { |
| 29 | if (isset($_GET['page']) && sanitize_key(wp_unslash($_GET['page'])) === self::PAGE_SLUG) { |
| 30 | remove_all_actions('user_admin_notices'); |
| 31 | remove_all_actions('admin_notices'); |
| 32 | remove_all_actions('all_admin_notices'); |
| 33 | } |
| 34 | }, 999); |
| 35 | } |
| 36 | |
| 37 | public static function add_menu() |
| 38 | { |
| 39 | add_submenu_page( |
| 40 | 'embedpress', |
| 41 | __('EmbedPress Google Reviews', 'embedpress'), |
| 42 | __('Google Reviews', 'embedpress'), |
| 43 | 'manage_options', |
| 44 | self::PAGE_SLUG, |
| 45 | [__CLASS__, 'render'] |
| 46 | ); |
| 47 | } |
| 48 | |
| 49 | public static function render() |
| 50 | { |
| 51 | if (!current_user_can('manage_options')) { |
| 52 | wp_die(esc_html__('You do not have permission to access this page.', 'embedpress')); |
| 53 | } |
| 54 | |
| 55 | // Frontend renderer assets — the live-preview HTML renders inside the React app, |
| 56 | // so it needs the same CSS *and JS* the public-facing shortcode uses. The JS |
| 57 | // carries a MutationObserver that auto-initializes carousel/slider/marquee/ |
| 58 | // spotlight layouts when the preview HTML is injected into the DOM (without it |
| 59 | // those JS-driven layouts render as a raw, overlapping flex track). |
| 60 | GoogleReviewsRenderer::enqueue_assets(); |
| 61 | |
| 62 | // Let EmbedPress Pro load its preview assets (google-reviews-pro.css/js) on |
| 63 | // this settings page too — otherwise the live preview renders the Pro |
| 64 | // layouts (masonry/badge/spotlight/knowledge/marquee/bubble + dark theme) |
| 65 | // with the right HTML/classes but no Pro CSS, so they fall back to looking |
| 66 | // like a plain list. Pro hooks this in Google_Reviews_Pro. |
| 67 | do_action('embedpress/google_reviews/admin_preview_assets'); |
| 68 | |
| 69 | // Pass REST config to the React app. |
| 70 | $pro_active = function_exists('is_embedpress_pro_active') && is_embedpress_pro_active(); |
| 71 | $bootstrap = [ |
| 72 | 'restUrl' => esc_url_raw(rest_url(GoogleReviewsRestController::NS . '/google-reviews')), |
| 73 | 'nonce' => wp_create_nonce('wp_rest'), |
| 74 | // Drives the shortcode generator's free/Pro split — Pro controls render |
| 75 | // locked (greyed + crown + upgrade link) until Pro is active, mirroring |
| 76 | // the Gutenberg block inspector exactly. |
| 77 | 'proActive' => $pro_active, |
| 78 | 'upgradeUrl' => 'https://wpdeveloper.com/in/upgrade-embedpress', |
| 79 | ]; |
| 80 | |
| 81 | // Inline the bootstrap before the React entry executes. AssetManager enqueues |
| 82 | // the script with handle `embedpress-google-reviews-admin` on this page. |
| 83 | wp_add_inline_script( |
| 84 | 'embedpress-google-reviews-admin', |
| 85 | 'window.epGoogleReviewsAdmin = ' . wp_json_encode($bootstrap) . ';', |
| 86 | 'before' |
| 87 | ); |
| 88 | ?> |
| 89 | <div id="embedpress-google-reviews-root"> |
| 90 | <noscript> |
| 91 | <div class="notice notice-warning"><p><?php esc_html_e('JavaScript is required to use this page.', 'embedpress'); ?></p></div> |
| 92 | </noscript> |
| 93 | </div> |
| 94 | <?php |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Legacy server-rendered page kept as a graceful fallback if the React bundle |
| 99 | * fails to mount (e.g. JS disabled). |
| 100 | */ |
| 101 | public static function render_legacy_fallback() |
| 102 | { |
| 103 | $rest_url = esc_url_raw(rest_url(GoogleReviewsRestController::NS . '/google-reviews')); |
| 104 | $nonce = wp_create_nonce('wp_rest'); |
| 105 | ?> |
| 106 | <div class="wrap ep-gr-admin"> |
| 107 | <h1><?php esc_html_e('EmbedPress — Google Reviews', 'embedpress'); ?></h1> |
| 108 | <p class="description"><?php esc_html_e('Configure your Google Places API key and build a shortcode you can paste into any page or post.', 'embedpress'); ?></p> |
| 109 | |
| 110 | <div class="ep-gr-admin-grid"> |
| 111 | <!-- Configuration card --> |
| 112 | <div class="ep-gr-card"> |
| 113 | <h2><?php esc_html_e('Configuration', 'embedpress'); ?></h2> |
| 114 | |
| 115 | <p class="ep-gr-field"> |
| 116 | <label for="ep-gr-api-key"><strong><?php esc_html_e('Google Places API key', 'embedpress'); ?></strong></label> |
| 117 | <input type="text" id="ep-gr-api-key" class="regular-text" autocomplete="off" placeholder="AIza…" /> |
| 118 | <span class="description"><?php |
| 119 | printf( |
| 120 | /* translators: %s: link to Google Cloud Console */ |
| 121 | wp_kses(__('Get a key from <a href="%s" target="_blank" rel="noopener">Google Cloud Console</a>. Enable both <code>Places API</code> and <code>Places API (New)</code>.', 'embedpress'), ['a' => ['href' => true, 'target' => true, 'rel' => true], 'code' => []]), |
| 122 | 'https://console.cloud.google.com/apis/library/places-backend.googleapis.com' |
| 123 | ); |
| 124 | ?></span> |
| 125 | </p> |
| 126 | |
| 127 | <p class="ep-gr-field"> |
| 128 | <label for="ep-gr-cache-ttl"><strong><?php esc_html_e('Cache reviews for', 'embedpress'); ?></strong></label> |
| 129 | <select id="ep-gr-cache-ttl"> |
| 130 | <option value="3600">1 <?php esc_html_e('hour', 'embedpress'); ?></option> |
| 131 | <option value="21600" selected>6 <?php esc_html_e('hours', 'embedpress'); ?></option> |
| 132 | <option value="86400">24 <?php esc_html_e('hours', 'embedpress'); ?></option> |
| 133 | </select> |
| 134 | </p> |
| 135 | |
| 136 | <p> |
| 137 | <button type="button" class="button button-primary" id="ep-gr-save"><?php esc_html_e('Save changes', 'embedpress'); ?></button> |
| 138 | <button type="button" class="button" id="ep-gr-clear-cache"><?php esc_html_e('Clear reviews cache', 'embedpress'); ?></button> |
| 139 | <span class="ep-gr-status" id="ep-gr-config-status" aria-live="polite"></span> |
| 140 | </p> |
| 141 | </div> |
| 142 | |
| 143 | <!-- Shortcode generator card --> |
| 144 | <div class="ep-gr-card"> |
| 145 | <h2><?php esc_html_e('Shortcode generator', 'embedpress'); ?></h2> |
| 146 | <p class="description"><?php esc_html_e('Search for a place, pick your options, and copy the shortcode.', 'embedpress'); ?></p> |
| 147 | |
| 148 | <p class="ep-gr-field ep-gr-picker-field"> |
| 149 | <label for="ep-gr-search"><strong><?php esc_html_e('Search for a place', 'embedpress'); ?></strong></label> |
| 150 | <input type="text" id="ep-gr-search" class="regular-text" placeholder="<?php esc_attr_e('e.g. Sydney Opera House', 'embedpress'); ?>" autocomplete="off" /> |
| 151 | <ul id="ep-gr-suggestions" class="ep-gr-suggestions" role="listbox" hidden></ul> |
| 152 | </p> |
| 153 | |
| 154 | <p class="ep-gr-selected" id="ep-gr-selected" hidden> |
| 155 | <strong><?php esc_html_e('Selected:', 'embedpress'); ?></strong> |
| 156 | <span id="ep-gr-selected-name"></span> |
| 157 | <code id="ep-gr-selected-id"></code> |
| 158 | <button type="button" class="button-link" id="ep-gr-clear-selection"><?php esc_html_e('Change', 'embedpress'); ?></button> |
| 159 | </p> |
| 160 | |
| 161 | <div class="ep-gr-options-grid"> |
| 162 | <p class="ep-gr-field"> |
| 163 | <label for="ep-gr-limit"><?php esc_html_e('Limit', 'embedpress'); ?></label> |
| 164 | <select id="ep-gr-limit"> |
| 165 | <option value="1">1</option><option value="2">2</option><option value="3" selected>3</option><option value="4">4</option><option value="5">5</option> |
| 166 | </select> |
| 167 | </p> |
| 168 | <p class="ep-gr-field"> |
| 169 | <label for="ep-gr-min-rating"><?php esc_html_e('Minimum rating', 'embedpress'); ?></label> |
| 170 | <select id="ep-gr-min-rating"> |
| 171 | <option value="0"><?php esc_html_e('Any', 'embedpress'); ?></option> |
| 172 | <option value="3">3+</option><option value="4">4+</option><option value="5">5</option> |
| 173 | </select> |
| 174 | </p> |
| 175 | <p class="ep-gr-field"> |
| 176 | <label for="ep-gr-layout"><?php esc_html_e('Layout', 'embedpress'); ?></label> |
| 177 | <select id="ep-gr-layout"> |
| 178 | <option value="list" selected><?php esc_html_e('List', 'embedpress'); ?></option> |
| 179 | <option value="grid"><?php esc_html_e('Grid', 'embedpress'); ?></option> |
| 180 | <option value="carousel"><?php esc_html_e('Carousel', 'embedpress'); ?></option> |
| 181 | </select> |
| 182 | </p> |
| 183 | </div> |
| 184 | |
| 185 | <fieldset class="ep-gr-toggles"> |
| 186 | <legend><?php esc_html_e('Show', 'embedpress'); ?></legend> |
| 187 | <label><input type="checkbox" id="ep-gr-show-photo" checked /> <?php esc_html_e('Photo', 'embedpress'); ?></label> |
| 188 | <label><input type="checkbox" id="ep-gr-show-stars" checked /> <?php esc_html_e('Stars', 'embedpress'); ?></label> |
| 189 | <label><input type="checkbox" id="ep-gr-show-date" checked /> <?php esc_html_e('Date', 'embedpress'); ?></label> |
| 190 | <label><input type="checkbox" id="ep-gr-show-link" checked /> <?php esc_html_e('"View on Google" link', 'embedpress'); ?></label> |
| 191 | </fieldset> |
| 192 | |
| 193 | <p class="ep-gr-field"> |
| 194 | <label for="ep-gr-shortcode"><strong><?php esc_html_e('Generated shortcode', 'embedpress'); ?></strong></label> |
| 195 | <textarea id="ep-gr-shortcode" rows="2" class="large-text code" readonly></textarea> |
| 196 | <button type="button" class="button" id="ep-gr-copy"><?php esc_html_e('Copy', 'embedpress'); ?></button> |
| 197 | <span class="ep-gr-status" id="ep-gr-copy-status" aria-live="polite"></span> |
| 198 | </p> |
| 199 | |
| 200 | <h3><?php esc_html_e('Live preview', 'embedpress'); ?></h3> |
| 201 | <div id="ep-gr-preview" class="ep-gr-preview-wrap"> |
| 202 | <p class="ep-gr-preview-empty"><?php esc_html_e('Search for a place above to see a live preview here.', 'embedpress'); ?></p> |
| 203 | </div> |
| 204 | </div> |
| 205 | </div> |
| 206 | </div> |
| 207 | <?php self::render_styles(); ?> |
| 208 | <script> |
| 209 | (function(){ |
| 210 | var REST = <?php echo wp_json_encode($rest_url); ?>; |
| 211 | var NONCE = <?php echo wp_json_encode($nonce); ?>; |
| 212 | var $ = function(id){ return document.getElementById(id); }; |
| 213 | |
| 214 | function api(method, path, params, body) { |
| 215 | var url = REST + path; |
| 216 | if (params) { |
| 217 | var qs = Object.keys(params).map(function(k){ return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&'); |
| 218 | if (qs) url += '?' + qs; |
| 219 | } |
| 220 | var init = { |
| 221 | method: method, |
| 222 | headers: { 'X-WP-Nonce': NONCE, 'Content-Type': 'application/json' }, |
| 223 | credentials: 'same-origin' |
| 224 | }; |
| 225 | if (body) init.body = JSON.stringify(body); |
| 226 | return fetch(url, init).then(function(r){ return r.json().then(function(d){ return { ok: r.ok, status: r.status, body: d }; }); }); |
| 227 | } |
| 228 | |
| 229 | // --- Settings load + save --- |
| 230 | function loadSettings(){ |
| 231 | api('GET', '/settings').then(function(res){ |
| 232 | if (!res.ok) return; |
| 233 | if (res.body.api_key_configured) $('ep-gr-api-key').placeholder = res.body.api_key_masked || '***'; |
| 234 | if (res.body.cache_ttl) $('ep-gr-cache-ttl').value = String(res.body.cache_ttl); |
| 235 | }); |
| 236 | } |
| 237 | $('ep-gr-save').addEventListener('click', function(){ |
| 238 | var key = $('ep-gr-api-key').value; |
| 239 | var ttl = parseInt($('ep-gr-cache-ttl').value, 10); |
| 240 | var payload = { cache_ttl: ttl }; |
| 241 | if (key && key.trim() !== '') payload.api_key = key.trim(); |
| 242 | api('POST', '/settings', null, payload).then(function(res){ |
| 243 | var s = $('ep-gr-config-status'); |
| 244 | if (res.ok) { |
| 245 | s.textContent = '<?php echo esc_js(__('Saved.', 'embedpress')); ?>'; |
| 246 | s.style.color = '#15803d'; |
| 247 | $('ep-gr-api-key').value = ''; |
| 248 | if (res.body.api_key_masked) $('ep-gr-api-key').placeholder = res.body.api_key_masked; |
| 249 | } else { |
| 250 | s.textContent = (res.body && res.body.message) || '<?php echo esc_js(__('Save failed.', 'embedpress')); ?>'; |
| 251 | s.style.color = '#b91c1c'; |
| 252 | } |
| 253 | setTimeout(function(){ s.textContent = ''; }, 3000); |
| 254 | }); |
| 255 | }); |
| 256 | $('ep-gr-clear-cache').addEventListener('click', function(){ |
| 257 | api('POST', '/clear-cache', null, {}).then(function(res){ |
| 258 | var s = $('ep-gr-config-status'); |
| 259 | s.textContent = res.ok ? ('<?php echo esc_js(__('Cleared', 'embedpress')); ?> (' + (res.body.deleted || 0) + ')') : '<?php echo esc_js(__('Failed.', 'embedpress')); ?>'; |
| 260 | s.style.color = res.ok ? '#15803d' : '#b91c1c'; |
| 261 | setTimeout(function(){ s.textContent = ''; }, 3000); |
| 262 | }); |
| 263 | }); |
| 264 | |
| 265 | // --- Searchable place picker --- |
| 266 | var debounce; |
| 267 | var selected = null; |
| 268 | $('ep-gr-search').addEventListener('input', function(e){ |
| 269 | clearTimeout(debounce); |
| 270 | var q = e.target.value.trim(); |
| 271 | if (q.length < 2) { $('ep-gr-suggestions').hidden = true; return; } |
| 272 | debounce = setTimeout(function(){ |
| 273 | api('GET', '/search', { q: q }).then(function(res){ |
| 274 | var ul = $('ep-gr-suggestions'); |
| 275 | ul.innerHTML = ''; |
| 276 | if (!res.ok || !res.body.predictions || res.body.predictions.length === 0) { |
| 277 | ul.hidden = true; |
| 278 | return; |
| 279 | } |
| 280 | res.body.predictions.forEach(function(p){ |
| 281 | var li = document.createElement('li'); |
| 282 | li.setAttribute('role', 'option'); |
| 283 | li.tabIndex = 0; |
| 284 | li.innerHTML = '<strong></strong><br><span></span>'; |
| 285 | li.querySelector('strong').textContent = p.main_text || p.description; |
| 286 | li.querySelector('span').textContent = p.secondary_text || ''; |
| 287 | li.addEventListener('click', function(){ pick(p); }); |
| 288 | li.addEventListener('keydown', function(ev){ if (ev.key === 'Enter') pick(p); }); |
| 289 | ul.appendChild(li); |
| 290 | }); |
| 291 | ul.hidden = false; |
| 292 | }); |
| 293 | }, 250); |
| 294 | }); |
| 295 | |
| 296 | function pick(p) { |
| 297 | selected = p; |
| 298 | $('ep-gr-search').value = ''; |
| 299 | $('ep-gr-suggestions').hidden = true; |
| 300 | $('ep-gr-selected').hidden = false; |
| 301 | $('ep-gr-selected-name').textContent = p.main_text + (p.secondary_text ? ' — ' + p.secondary_text : ''); |
| 302 | $('ep-gr-selected-id').textContent = p.place_id; |
| 303 | regenerate(); |
| 304 | } |
| 305 | $('ep-gr-clear-selection').addEventListener('click', function(){ |
| 306 | selected = null; |
| 307 | $('ep-gr-selected').hidden = true; |
| 308 | $('ep-gr-shortcode').value = ''; |
| 309 | $('ep-gr-preview').innerHTML = '<p class="ep-gr-preview-empty"><?php echo esc_js(__('Search for a place above to see a live preview here.', 'embedpress')); ?></p>'; |
| 310 | }); |
| 311 | |
| 312 | // --- Shortcode generator + live preview --- |
| 313 | var controls = ['ep-gr-limit','ep-gr-min-rating','ep-gr-layout','ep-gr-show-photo','ep-gr-show-stars','ep-gr-show-date','ep-gr-show-link']; |
| 314 | controls.forEach(function(id){ $(id).addEventListener('change', regenerate); }); |
| 315 | |
| 316 | function buildAttrs(){ |
| 317 | if (!selected) return null; |
| 318 | return { |
| 319 | place_id: selected.place_id, |
| 320 | place_name: selected.main_text || '', |
| 321 | limit: parseInt($('ep-gr-limit').value, 10), |
| 322 | min_rating: parseInt($('ep-gr-min-rating').value, 10), |
| 323 | layout: $('ep-gr-layout').value, |
| 324 | show_photo: $('ep-gr-show-photo').checked, |
| 325 | show_stars: $('ep-gr-show-stars').checked, |
| 326 | show_date: $('ep-gr-show-date').checked, |
| 327 | show_link: $('ep-gr-show-link').checked, |
| 328 | }; |
| 329 | } |
| 330 | |
| 331 | function buildShortcode(attrs) { |
| 332 | var defaults = { limit: 5, min_rating: 0, layout: 'list', show_photo: true, show_stars: true, show_date: true, show_link: true }; |
| 333 | var parts = ['embedpress_google_reviews', 'place_id="' + attrs.place_id + '"']; |
| 334 | if (attrs.place_name) parts.push('place_name="' + attrs.place_name.replace(/"/g, '"') + '"'); |
| 335 | ['limit','min_rating','layout','show_photo','show_stars','show_date','show_link'].forEach(function(k){ |
| 336 | var v = attrs[k]; |
| 337 | if (v === defaults[k]) return; |
| 338 | if (typeof v === 'boolean') v = v ? 'true' : 'false'; |
| 339 | parts.push(k + '="' + v + '"'); |
| 340 | }); |
| 341 | return '[' + parts.join(' ') + ']'; |
| 342 | } |
| 343 | |
| 344 | var previewDebounce; |
| 345 | function regenerate(){ |
| 346 | var attrs = buildAttrs(); |
| 347 | if (!attrs) return; |
| 348 | $('ep-gr-shortcode').value = buildShortcode(attrs); |
| 349 | clearTimeout(previewDebounce); |
| 350 | previewDebounce = setTimeout(function(){ |
| 351 | api('GET', '/preview', attrs).then(function(res){ |
| 352 | if (res.ok && res.body.html) { |
| 353 | $('ep-gr-preview').innerHTML = res.body.html; |
| 354 | } else { |
| 355 | $('ep-gr-preview').innerHTML = '<p class="ep-gr-preview-empty">' + ((res.body && res.body.message) || '<?php echo esc_js(__('Preview unavailable.', 'embedpress')); ?>') + '</p>'; |
| 356 | } |
| 357 | }); |
| 358 | }, 250); |
| 359 | } |
| 360 | |
| 361 | $('ep-gr-copy').addEventListener('click', function(){ |
| 362 | var ta = $('ep-gr-shortcode'); |
| 363 | if (!ta.value) return; |
| 364 | ta.select(); |
| 365 | try { |
| 366 | navigator.clipboard.writeText(ta.value); |
| 367 | $('ep-gr-copy-status').textContent = '<?php echo esc_js(__('Copied!', 'embedpress')); ?>'; |
| 368 | } catch (e) { |
| 369 | document.execCommand('copy'); |
| 370 | $('ep-gr-copy-status').textContent = '<?php echo esc_js(__('Copied!', 'embedpress')); ?>'; |
| 371 | } |
| 372 | setTimeout(function(){ $('ep-gr-copy-status').textContent = ''; }, 2000); |
| 373 | }); |
| 374 | |
| 375 | loadSettings(); |
| 376 | })(); |
| 377 | </script> |
| 378 | <?php |
| 379 | } |
| 380 | |
| 381 | private static function render_styles() |
| 382 | { |
| 383 | ?> |
| 384 | <style> |
| 385 | .ep-gr-admin-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; } |
| 386 | @media (max-width: 1100px) { .ep-gr-admin-grid { grid-template-columns: 1fr; } } |
| 387 | .ep-gr-card { background: #fff; border: 1px solid #c3c4c7; padding: 16px 20px; border-radius: 4px; } |
| 388 | .ep-gr-card h2 { margin-top: 0; } |
| 389 | .ep-gr-field { display: flex; flex-direction: column; gap: 4px; margin: 12px 0; position: relative; } |
| 390 | .ep-gr-field label { font-size: 13px; } |
| 391 | .ep-gr-options-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } |
| 392 | .ep-gr-toggles { display: flex; gap: 16px; flex-wrap: wrap; padding: 0; border: 0; margin: 12px 0; } |
| 393 | .ep-gr-toggles legend { font-weight: 600; padding: 0; margin-bottom: 6px; } |
| 394 | .ep-gr-toggles label { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; } |
| 395 | .ep-gr-suggestions { position: absolute; top: 100%; left: 0; right: 0; max-height: 260px; overflow-y: auto; background: #fff; border: 1px solid #c3c4c7; margin: 0; padding: 0; list-style: none; z-index: 10; box-shadow: 0 4px 12px rgba(0,0,0,0.08); } |
| 396 | .ep-gr-suggestions li { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #f0f0f1; } |
| 397 | .ep-gr-suggestions li:hover, .ep-gr-suggestions li:focus { background: #f0f6fc; outline: none; } |
| 398 | .ep-gr-selected { padding: 8px 12px; background: #f0f6fc; border-radius: 4px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } |
| 399 | .ep-gr-selected code { font-size: 11px; color: #6b7280; } |
| 400 | .ep-gr-status { margin-left: 8px; font-size: 13px; font-weight: 500; } |
| 401 | .ep-gr-preview-wrap { min-height: 120px; padding: 12px; border: 1px dashed #c3c4c7; border-radius: 4px; background: #f6f7f7; } |
| 402 | .ep-gr-preview-empty { color: #6b7280; font-style: italic; text-align: center; margin: 24px 0; } |
| 403 | #ep-gr-shortcode { font-family: monospace; } |
| 404 | </style> |
| 405 | <?php |
| 406 | } |
| 407 | } |
| 408 |