PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Includes / Classes / GoogleReviewsAdminPage.php
embedpress / EmbedPress / Includes / Classes Last commit date
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, '&quot;') + '"');
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