PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.10.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / admin / class-post-list-columns.php

class-post-list-columns.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.10.0, at includes/admin/class-post-list-columns.php

725 lines 27.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Post List Columns Class
5 *
6 * Handles custom columns in WordPress admin post list tables
7 *
8 * @package ThinkRank\Admin
9 * @since 1.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace ThinkRank\Admin;
15
16 // Prevent direct access
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * Post List Columns Class
23 *
24 * Single Responsibility: Manage custom columns in post list tables
25 *
26 * @since 1.0.0
27 */
28 class Post_List_Columns {
29
30 /**
31 * Column ID
32 *
33 * @var string
34 */
35 private const COLUMN_ID = 'thinkrank_seo_overview';
36
37 /**
38 * Initialize post list columns
39 *
40 * @return void
41 */
42 public function init(): void {
43 // Get all public post types
44 add_action('admin_init', function () {
45 $post_types = $this->get_supported_post_types();
46
47 foreach ($post_types as $post_type) {
48 // Add column header
49 add_filter("manage_{$post_type}_posts_columns", [$this, 'add_column_header'], 10);
50
51 // Add column content
52 add_action("manage_{$post_type}_posts_custom_column", [$this, 'render_column_content'], 10, 2);
53
54 // Make column sortable
55 add_filter("manage_edit-{$post_type}_sortable_columns", [$this, 'make_column_sortable'], 10);
56 }
57 });
58
59 // Handle sorting
60 add_action('pre_get_posts', [$this, 'handle_column_sorting']);
61
62 // Enqueue admin styles for column
63 add_action('admin_enqueue_scripts', [$this, 'enqueue_column_styles']);
64 }
65
66 /**
67 * Get supported post types
68 *
69 * @return array Array of post type names
70 */
71 private function get_supported_post_types(): array {
72 $post_types = get_post_types(['public' => true], 'names');
73
74 // Remove unnecessary post types
75 unset($post_types['attachment']);
76 unset($post_types['elementor_library']);
77 unset($post_types['oceanwp_library']);
78 unset($post_types['ae_global_templates']);
79
80 /**
81 * Filter supported post types for ThinkRank SEO Overview column
82 *
83 * @param array $post_types Array of post type names
84 */
85 return apply_filters('thinkrank_seo_overview_post_types', $post_types);
86 }
87
88 /**
89 * Add column header
90 *
91 * @param array $columns Existing columns
92 * @return array Modified columns
93 */
94 public function add_column_header(array $columns): array {
95 // Insert column before date column
96 $new_columns = [];
97
98 foreach ($columns as $key => $value) {
99 if ($key === 'date') {
100 $new_columns[self::COLUMN_ID] = __('ThinkRank SEO Overview', 'thinkrank');
101 }
102 $new_columns[$key] = $value;
103 }
104
105 // If date column doesn't exist, add at the end
106 if (!isset($columns['date'])) {
107 $new_columns[self::COLUMN_ID] = __('ThinkRank SEO Overview', 'thinkrank');
108 }
109
110 return $new_columns;
111 }
112
113 /**
114 * Render column content
115 *
116 * @param string $column_name Column name
117 * @param int $post_id Post ID
118 * @return void
119 */
120 public function render_column_content(string $column_name, int $post_id): void {
121 if ($column_name !== self::COLUMN_ID) {
122 return;
123 }
124
125 // Get SEO data
126 $seo_data = $this->get_seo_data($post_id);
127
128 // Render the column content
129 $this->render_seo_overview($seo_data, $post_id);
130 }
131
132 /**
133 * Get SEO data for a post
134 *
135 * @param int $post_id Post ID
136 * @return array SEO data
137 */
138 private function get_seo_data(int $post_id): array {
139 // Get SEO score and metrics from database table
140 $seo_metrics = $this->get_seo_metrics_from_database($post_id);
141
142 // Get focus keyword
143 $focus_keyword = get_post_meta($post_id, '_thinkrank_focus_keyword', true);
144
145 // Get meta title
146 $meta_title = get_post_meta($post_id, '_thinkrank_seo_title', true);
147
148 // Get meta description
149 $meta_description = get_post_meta($post_id, '_thinkrank_meta_description', true);
150
151 // Get content quality and readability from database columns
152 $content_quality = $seo_metrics['content_quality'] ?? 'Not Analyzed';
153 $readability = $seo_metrics['readability_score'] ?? 'N/A';
154
155 return [
156 'seo_score' => $seo_metrics['seo_score'],
157 'seo_grade' => $seo_metrics['seo_grade'],
158 'content_quality' => $content_quality,
159 'readability' => $readability,
160 'focus_keyword' => !empty($focus_keyword),
161 'focus_keyword_value' => $focus_keyword,
162 'meta_title' => !empty($meta_title),
163 'meta_description' => !empty($meta_description),
164 'pillar_content' => $this->is_link_suggestions_enabled(get_post_type($post_id)) && get_post_meta($post_id, '_thinkrank_pillar_content', true) === '1',
165 ];
166 }
167
168 /**
169 * Check if link suggestions are enabled for a post type
170 *
171 * @param string $post_type Post type
172 * @return bool True if enabled
173 */
174 private function is_link_suggestions_enabled(string $post_type): bool {
175 $settings = get_option('thinkrank_global_seo_settings', []);
176
177 if (isset($settings[$post_type]['link_suggestions'])) {
178 return (bool) $settings[$post_type]['link_suggestions'];
179 }
180
181 return true;
182 }
183
184 /**
185 * Get SEO metrics from database table
186 *
187 * @param int $post_id Post ID
188 * @return array SEO metrics (seo_score, content_quality, readability_score)
189 */
190 private function get_seo_metrics_from_database(int $post_id): array {
191 global $wpdb;
192
193 $table_name = $wpdb->prefix . 'thinkrank_seo_scores';
194
195 // Default values
196 $default_metrics = [
197 'seo_score' => null,
198 'seo_grade' => null,
199 'content_quality' => null,
200 'readability_score' => null,
201 ];
202
203 // Check if table exists
204 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SEO score retrieval requires direct database access
205 $table_exists = $wpdb->get_var($wpdb->prepare(
206 "SHOW TABLES LIKE %s",
207 $table_name
208 ));
209
210 if (!$table_exists) {
211 return $default_metrics;
212 }
213
214 // Get the most recent metrics from the database
215 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SEO score retrieval requires direct database access
216 $result = $wpdb->get_row($wpdb->prepare(
217 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is properly escaped
218 "SELECT overall_score, grade, content_quality, readability_score FROM `{$table_name}` WHERE post_id = %d ORDER BY created_at DESC LIMIT 1",
219 $post_id
220 ), ARRAY_A);
221
222 if (!$result) {
223 return $default_metrics;
224 }
225
226 return [
227 'seo_score' => $result['overall_score'] !== null ? intval($result['overall_score']) : null,
228 'seo_grade' => $result['grade'] !== null ? $result['grade'] : null,
229 'content_quality' => $result['content_quality'],
230 'readability_score' => $result['readability_score'],
231 ];
232 }
233
234 /**
235 * Render SEO overview content
236 *
237 * @param array $seo_data SEO data
238 * @param int $post_id Post ID
239 * @return void
240 */
241 private function render_seo_overview(array $seo_data, int $post_id): void {
242 ?>
243 <div class="thinkrank-seo-overview">
244 <div class="thinkrank-seo-row thinkrank-seo-row-top">
245 <?php if ($seo_data['seo_score'] !== null): ?>
246 <div class="thinkrank-seo-badge thinkrank-score-<?php echo esc_attr($this->get_score_class($seo_data['seo_score'])); ?>"
247 title="<?php esc_attr_e('SEO Score', 'thinkrank'); ?>">
248 <?php echo esc_html($seo_data['seo_score']); ?>% <?php echo esc_html($seo_data['seo_grade']); ?>
249 </div>
250 <?php else: ?>
251 <div class="thinkrank-seo-badge thinkrank-not-set" title="<?php esc_attr_e('SEO Score', 'thinkrank'); ?>">
252 <?php esc_html_e('N/A', 'thinkrank'); ?>
253 </div>
254 <?php endif; ?>
255
256 <div class="thinkrank-seo-separator"></div>
257
258 <div class="thinkrank-seo-badge thinkrank-status-<?php echo esc_attr(sanitize_html_class(strtolower(str_replace(' ', '-', $seo_data['content_quality'])))); ?>"
259 title="<?php esc_attr_e('Content Quality', 'thinkrank'); ?>">
260 <?php echo esc_html($seo_data['content_quality']); ?>
261 </div>
262
263 <?php if ($seo_data['pillar_content']): ?>
264 <div class="thinkrank-seo-separator"></div>
265 <div class="thinkrank-seo-badge thinkrank-pillar-icon" title="<?php esc_attr_e('Pillar Content', 'thinkrank'); ?>">
266 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
267 <path d="M2.5 11V3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
268 <path d="M6 11V3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
269 <path d="M9.5 11V3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
270 <path d="M1.5 3.5H10.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
271 <path d="M2 3.5L6 1L10 3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
272 <path d="M1 11H11" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
273 </svg>
274 </div>
275 <?php endif; ?>
276 </div>
277
278 <div class="thinkrank-seo-row thinkrank-seo-row-middle">
279 <div class="thinkrank-focus-keyword-item" data-post-id="<?php echo esc_attr($post_id); ?>">
280 <span class="thinkrank-focus-keyword-display <?php echo $seo_data['focus_keyword'] ? 'thinkrank-set' : 'thinkrank-not-set'; ?>"
281 data-keyword="<?php echo esc_attr($seo_data['focus_keyword_value']); ?>"
282 title="<?php esc_attr_e('Focus Keyword - Click to edit', 'thinkrank'); ?>">
283 <?php echo $seo_data['focus_keyword'] ? esc_html($seo_data['focus_keyword_value']) : esc_html__('Not Set', 'thinkrank'); ?>
284 </span>
285 <svg class="thinkrank-edit-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
286 <path d="M8 2H3.333A1.333 1.333 0 0 0 2 3.333v9.334A1.333 1.333 0 0 0 3.333 14h9.334A1.334 1.334 0 0 0 14 12.667V8" stroke="#A5AAAC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
287 <path d="M12.25 1.75a1.414 1.414 0 1 1 2 2L8.24 9.758a1.334 1.334 0 0 1-.568.336l-1.915.56a.334.334 0 0 1-.414-.413l.56-1.915c.063-.215.18-.41.338-.568l6.008-6.01Z" stroke="#A5AAAC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
288 </svg>
289 <svg class="thinkrank-success-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
290 <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm4.486 4.515a.688.688 0 0 0-.972 0L6.5 9.528 4.486 7.515a.688.688 0 0 0-.972.972l2.5 2.5a.688.688 0 0 0 .972 0l5.5-5.5a.688.688 0 0 0 0-.972Z" fill="#4451FF" />
291 </svg>
292 <span class="thinkrank-focus-keyword-loading" style="display:none;">
293 <span class="spinner is-active" style="float:none;margin:0;"></span>
294 </span>
295 </div>
296 </div>
297
298 <div class="thinkrank-seo-row thinkrank-seo-row-bottom">
299 <div class="thinkrank-seo-metric" title="<?php esc_attr_e('Readability', 'thinkrank'); ?>">
300 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
301 <path d="M6.74999 11.75C6.74999 13.1307 5.63071 14.25 4.24999 14.25C2.86928 14.25 1.75 13.1307 1.75 11.75C1.75 10.3693 2.86928 9.25001 4.24999 9.25001C5.63071 9.25001 6.74999 10.3693 6.74999 11.75Z" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
302 <path d="M14.25 11.75C14.25 13.1307 13.1307 14.25 11.75 14.25C10.3693 14.25 9.25 13.1307 9.25 11.75C9.25 10.3693 10.3693 9.25001 11.75 9.25001C13.1307 9.25001 14.25 10.3693 14.25 11.75Z" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
303 <path d="M0 11.75H1.74999" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
304 <path d="M6.75 11.75H9.24999" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
305 <path d="M14.25 11.75H16" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
306 <path d="M3 7.375V3.00001C3 2.30967 3.55965 1.75002 4.25 1.75002H11.75C12.4403 1.75002 13 2.30967 13 3.00001V7.375" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
307 <path d="M4.875 4.25H11.125" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
308 <path d="M4.875 6.75H11.125" stroke="#373D44" stroke-width="1.5" stroke-miterlimit="10" />
309 </svg>
310 <span class="thinkrank-metric-value"><?php echo esc_html($seo_data['readability']); ?></span>
311 </div>
312
313 <div class="thinkrank-seo-separator"></div>
314
315 <div class="thinkrank-seo-metric" title="<?php esc_attr_e('Meta Title', 'thinkrank'); ?>">
316 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
317 <path d="M10 3.33398H14" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
318 <path d="M10 8H14" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
319 <path d="M2 12.666H14" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
320 <path d="M2 7.99933L4.36867 2.84999C4.39638 2.7947 4.43893 2.74821 4.49156 2.71572C4.54419 2.68322 4.60482 2.66602 4.66667 2.66602C4.72852 2.66602 4.78915 2.68322 4.84177 2.71572C4.8944 2.74821 4.93695 2.7947 4.96467 2.84999L7.33333 7.99933" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
321 <path d="M2.61328 6.66602H6.71995" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
322 </svg>
323 <span class="thinkrank-metric-value"><?php echo $seo_data['meta_title'] ? esc_html__('Set', 'thinkrank') : esc_html__('Not Set', 'thinkrank'); ?></span>
324 </div>
325
326 <div class="thinkrank-seo-separator"></div>
327
328 <div class="thinkrank-seo-metric" title="<?php esc_attr_e('Meta Description', 'thinkrank'); ?>">
329 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
330 <path d="M4.0013 14.6673C3.64768 14.6673 3.30854 14.5268 3.05849 14.2768C2.80844 14.0267 2.66797 13.6876 2.66797 13.334V2.66732C2.66797 2.3137 2.80844 1.97456 3.05849 1.72451C3.30854 1.47446 3.64768 1.33399 4.0013 1.33399H9.33464C9.54567 1.33364 9.75469 1.37505 9.94966 1.45583C10.1446 1.53661 10.3217 1.65516 10.4706 1.80465L12.8626 4.19665C13.0125 4.34566 13.1314 4.52288 13.2124 4.71809C13.2934 4.91331 13.335 5.12263 13.3346 5.33399V13.334C13.3346 13.6876 13.1942 14.0267 12.9441 14.2768C12.6941 14.5268 12.3549 14.6673 12.0013 14.6673H4.0013Z" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
331 <path d="M6.66536 6H5.33203" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
332 <path d="M10.6654 8.66602H5.33203" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
333 <path d="M10.6654 11.334H5.33203" stroke="#373D44" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
334 </svg>
335 <span class="thinkrank-metric-value"><?php echo $seo_data['meta_description'] ? esc_html__('Set', 'thinkrank') : esc_html__('Not Set', 'thinkrank'); ?></span>
336 </div>
337 </div>
338 </div>
339 <?php
340 }
341
342 /**
343 * Get score class for styling
344 *
345 * @param int $score SEO score
346 * @return string CSS class
347 */
348 private function get_score_class(int $score): string {
349 if ($score >= 90) {
350 return 'great';
351 } elseif ($score >= 70) {
352 return 'good';
353 } elseif ($score >= 50) {
354 return 'medium';
355 } else {
356 return 'poor';
357 }
358 }
359
360 /**
361 * Make column sortable
362 *
363 * @param array $columns Sortable columns
364 * @return array Modified sortable columns
365 */
366 public function make_column_sortable(array $columns): array {
367 $columns[self::COLUMN_ID] = 'thinkrank_seo_score';
368 return $columns;
369 }
370
371 /**
372 * Handle column sorting
373 *
374 * @param \WP_Query $query WordPress query object
375 * @return void
376 */
377 public function handle_column_sorting(\WP_Query $query): void {
378 if (!is_admin() || !$query->is_main_query()) {
379 return;
380 }
381
382 $orderby = $query->get('orderby');
383
384 if ($orderby === 'thinkrank_seo_score') {
385 $query->set('meta_key', '_thinkrank_seo_score');
386 $query->set('orderby', 'meta_value_num');
387 }
388 }
389
390 /**
391 * Enqueue column styles
392 *
393 * @param string $hook_suffix Current admin page hook
394 * @return void
395 */
396 public function enqueue_column_styles(string $hook_suffix): void {
397 // Only load on post list pages (edit.php for all post types)
398 $screen = get_current_screen();
399 if (!$screen || $screen->base !== 'edit') {
400 return;
401 }
402
403 // Add inline styles to common admin styles
404 wp_add_inline_style('common', $this->get_column_styles());
405
406 // Enqueue inline editing script
407 wp_enqueue_script(
408 'thinkrank-focus-keyword-inline-edit',
409 THINKRANK_PLUGIN_URL . 'assets/focus-keyword-inline-edit.js',
410 ['jquery'],
411 THINKRANK_VERSION,
412 true
413 );
414
415 // Localize script with AJAX data
416 wp_localize_script(
417 'thinkrank-focus-keyword-inline-edit',
418 'thinkrankFocusKeyword',
419 [
420 'ajaxUrl' => admin_url('admin-ajax.php'),
421 'nonce' => wp_create_nonce(\ThinkRank\Admin\Focus_Keyword_Ajax::get_nonce_action()),
422 'action' => \ThinkRank\Admin\Focus_Keyword_Ajax::get_ajax_action(),
423 'i18n' => [
424 'notSet' => __('Not Set', 'thinkrank'),
425 'clickToEdit' => __('Click to edit', 'thinkrank'),
426 'pressEnter' => __('Press Enter to save, Esc to cancel', 'thinkrank'),
427 'saving' => __('Saving...', 'thinkrank'),
428 'saved' => __('Saved!', 'thinkrank'),
429 'error' => __('Error updating focus keyword. Please try again.', 'thinkrank'),
430 ]
431 ]
432 );
433 }
434
435 /**
436 * Get column CSS styles
437 *
438 * @return string CSS styles
439 */
440 private function get_column_styles(): string {
441 return '
442 .column-thinkrank_seo_overview {
443 width: 240px;
444 }
445
446 .thinkrank-seo-overview {
447 font-size: 13px;
448 line-height: 1.4;
449 display: flex;
450 flex-direction: column;
451 gap: 6px;
452 }
453
454 .thinkrank-seo-row {
455 display: flex;
456 align-items: center;
457 flex-wrap: wrap;
458 }
459
460 .thinkrank-seo-separator {
461 margin: 0 6px;
462 color: #c3c4c7;
463 font-size: 10px;
464 }
465
466 /* Top Row: Badges */
467 .thinkrank-seo-badge {
468 border-radius: 12px;
469 padding: 2px 8px;
470 font-weight: 600;
471 font-size: 12px;
472 display: inline-flex;
473 align-items: center;
474 white-space: nowrap;
475 position: relative;
476 }
477
478 .thinkrank-seo-badge::after,
479 .thinkrank-seo-metric::after {
480 content: attr(title);
481 position: absolute;
482 bottom: calc(100% + 11px);
483 left: 50%;
484 transform: translateX(-50%);
485 background: #00043D;
486 border-radius: 8px;
487 padding: 4px 8px;
488 color: #fff;
489 font-weight: 400;
490 box-shadow: 0 1px 2px rgba(17, 17, 17, 0.1);
491 visibility: hidden;
492 white-space: nowrap;
493 z-index: 20;
494 }
495
496 .thinkrank-seo-badge::before,
497 .thinkrank-seo-metric::before {
498 content: "";
499 position: absolute;
500 bottom: calc(100% + 7px);
501 left: calc(50% - 5px);
502 transform: rotate(45deg);
503 background: #00043D;
504 width: 10px;
505 height: 10px;
506 border-radius: 2px;
507 box-shadow: 0 1px 2px rgba(17, 17, 17, 0.1);
508 visibility: hidden;
509 z-index: 19;
510 }
511
512 .thinkrank-seo-badge:hover::before,
513 .thinkrank-seo-badge:hover::after {
514 visibility: visible;
515 }
516
517 /* Apply Z-Index for metric too */
518 .thinkrank-seo-metric:hover::before,
519 .thinkrank-seo-metric:hover::after {
520 visibility: visible;
521 }
522
523 .thinkrank-score-great {
524 background-color: #d1fae5;
525 color: #065f46;
526 }
527
528 .thinkrank-score-good {
529 background-color: #dbeafe;
530 color: #1e40af;
531 }
532
533 .thinkrank-score-medium {
534 background-color: #fef3c7;
535 color: #92400e;
536 }
537
538 .thinkrank-score-poor {
539 background-color: #fee2e2;
540 color: #b91c1c;
541 }
542
543 .thinkrank-status-good {
544 background-color: #E5FFF6;
545 color: #117953;
546 }
547
548 .thinkrank-status-ok {
549 background-color: #EBF0FF;
550 color: #1B42C2;
551 }
552
553 .thinkrank-status-not-analyzed,
554 .thinkrank-seo-badge.thinkrank-not-set {
555 background-color: #E5EBF0;
556 color: #58677D;
557 border-radius: 12px;
558 padding: 2px 8px;
559 }
560
561 .thinkrank-status-needs-improvement {
562 background-color: #FFF7EB;
563 color: #865A13;
564 }
565
566 .thinkrank-status-no-content {
567 background-color: #FFEBEE;
568 color: #EC113A;
569 }
570
571 .thinkrank-pillar-icon {
572 background-color: #F0F0F1;
573 color: #2271b1;
574 padding: 4px;
575 line-height: 20px;
576 }
577
578 /* Middle Row: Focus Keyword */
579 .thinkrank-focus-keyword-item {
580 width: 100%;
581 position: relative;
582 display: flex;
583 align-items: center;
584 background-color: #ECEEEE;
585 border-radius: 4px;
586 border: 1px solid #E4E6EC;
587 padding: 0 8px;
588 min-height: 24px;
589 }
590
591 .thinkrank-focus-keyword-display {
592 cursor: pointer;
593 flex-grow: 1;
594 white-space: nowrap;
595 overflow: hidden;
596 text-overflow: ellipsis;
597 color: #1d2327;
598 font-weight: 500;
599 }
600
601 .thinkrank-focus-keyword-display.thinkrank-not-set {
602 color: #646970;
603 font-style: italic;
604 }
605
606 .thinkrank-edit-icon {
607 font-size: 16px;
608 width: 16px;
609 height: 16px;
610 pointer-events: none;
611 position: absolute;
612 right: 8px;
613
614 path {
615 stroke: #A5AAAC;
616 }
617 }
618
619 .thinkrank-focus-keyword-item .thinkrank-success-icon {
620 position: absolute;
621 right: 8px;
622 visibility: hidden;
623 }
624
625 .thinkrank-focus-keyword-item:hover .thinkrank-edit-icon path {
626 stroke: #4451FF;
627 }
628
629 .thinkrank-focus-keyword-input[type="text"] {
630 width: 100%;
631 padding: 0 8px;
632 border: none;
633 border-radius: 4px;
634 font-size: 13px;
635 margin: 0;
636 min-height: 24px;
637 line-height: normal;
638 }
639
640 .thinkrank-focus-keyword-item.is-editing {
641 padding: 0;
642 background: transparent;
643 border-color: #5C91FE;
644 }
645
646 .thinkrank-focus-keyword-item.is-editing .thinkrank-focus-keyword-display,
647 .thinkrank-focus-keyword-item.is-editing .thinkrank-edit-icon {
648 display: none;
649 }
650
651 .thinkrank-focus-keyword-item.is-editing .thinkrank-success-icon {
652 visibility: visible;
653 }
654
655 .thinkrank-focus-keyword-item.is-editing.is-saving .thinkrank-success-icon {
656 visibility: hidden;
657 }
658
659 /* Bottom Row: Metrics */
660 .thinkrank-seo-metric {
661 display: flex;
662 align-items: center;
663 gap: 4px;
664 color: #646970;
665 font-size: 12px;
666 line-height: 20px;
667 position: relative;
668 }
669
670 .thinkrank-seo-metric:hover::before,
671 .thinkrank-seo-metric:hover::after {
672 visibility: visible;
673 }
674
675 .thinkrank-metric-value {
676 white-space: nowrap;
677 color: #505272;
678 }
679
680 /* Animations */
681 .thinkrank-focus-keyword-display.thinkrank-feedback-success {
682 animation: thinkrank-success-flash 0.5s ease-in-out;
683 }
684
685 .thinkrank-focus-keyword-display.thinkrank-feedback-error {
686 animation: thinkrank-error-flash 0.5s ease-in-out;
687 }
688
689 @keyframes thinkrank-success-flash {
690 0%, 100% { background-color: transparent; }
691 50% { background-color: #d1fae5; }
692 }
693
694 @keyframes thinkrank-error-flash {
695 0%, 100% { background-color: transparent; }
696 50% { background-color: #fee2e2; }
697 }
698
699 .thinkrank-inline-error {
700 position: absolute;
701 top: 100%;
702 left: 0;
703 z-index: 10;
704 color: #fff;
705 font-size: 11px;
706 margin-top: 4px;
707 padding: 4px 8px;
708 background-color: #d63638;
709 border-radius: 4px;
710 box-shadow: 0 2px 5px rgba(0,0,0,0.2);
711 }
712
713 .thinkrank-inline-error::after {
714 content: "";
715 position: absolute;
716 bottom: 100%;
717 left: 10px;
718 border-width: 5px;
719 border-style: solid;
720 border-color: transparent transparent #d63638 transparent;
721 }
722 ';
723 }
724 }
725