PluginProbe
Accessibility by AllAccessible / trunk
Accessibility by AllAccessible vtrunk
2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.6 trunk 1.0 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2 1.2.1 1.2.10 1.2.2 1.2.3 1.2.4 1.2.5 All 43 releases
allaccessible / inc / EditorMetaBox.php

EditorMetaBox.php in Accessibility by AllAccessible trunk, at inc/EditorMetaBox.php

842 lines 39.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AllAccessible Editor Meta Box
4 *
5 * Adds accessibility score meta box to post/page editor.
6 * Shows page-specific audit results and quick actions.
7 *
8 * @package AllAccessible
9 * @version 2.0.0
10 */
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 class AllAccessible_EditorMetaBox {
17
18 /**
19 * Singleton instance
20 */
21 private static $instance = null;
22
23 /**
24 * Get singleton instance
25 */
26 public static function get_instance() {
27 if (self::$instance === null) {
28 self::$instance = new self();
29 }
30 return self::$instance;
31 }
32
33 /**
34 * Constructor
35 */
36 private function __construct() {
37 add_action('add_meta_boxes', array($this, 'add_meta_box'));
38 add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
39 }
40
41 /**
42 * Add meta box to post/page editor
43 */
44 public function add_meta_box() {
45 $premium = (bool) get_option('aacb_accountID');
46
47 // Only show for premium users
48 if (!$premium) {
49 // Show upgrade prompt instead
50 add_meta_box(
51 'aacb_accessibility_score_upgrade',
52 __('Accessibility Score', 'allaccessible'),
53 array($this, 'render_upgrade_meta_box'),
54 array('post', 'page'),
55 'side',
56 'high'
57 );
58 return;
59 }
60
61 add_meta_box(
62 'aacb_accessibility_score',
63 __('Accessibility Score', 'allaccessible'),
64 array($this, 'render_meta_box'),
65 array('post', 'page'),
66 'side',
67 'high'
68 );
69 }
70
71 /**
72 * Enqueue scripts for meta box
73 */
74 public function enqueue_scripts($hook) {
75 // Only load on post/page editor
76 if (!in_array($hook, array('post.php', 'post-new.php'))) {
77 return;
78 }
79 wp_enqueue_style(
80 'aacx-v2-admin',
81 AACB_CSS . 'admin-v2.css',
82 array(),
83 aacb_asset_ver('admin-v2.css')
84 );
85
86 wp_enqueue_script(
87 'allaccessible-editor-metabox',
88 AACB_JS . 'js/editor-metabox.js',
89 array('jquery'),
90 aacb_asset_ver('js/editor-metabox.js'),
91 true
92 );
93
94 wp_localize_script('allaccessible-editor-metabox', 'aacbEditorMeta', array(
95 'nonce' => wp_create_nonce('wp_rest'),
96 'post_id' => get_the_ID(),
97 'labels' => array(
98 'loading' => __('Loading score...', 'allaccessible'),
99 'error' => __('Error loading score', 'allaccessible'),
100 'rescan_success' => __('Rescan initiated successfully', 'allaccessible'),
101 'rescan_error' => __('Error initiating rescan', 'allaccessible'),
102 ),
103 ));
104 }
105
106 /**
107 * Render accessibility score meta box.
108 */
109 public function render_meta_box($post) {
110 // Canonicalize the permalink before any API call so the canonical
111 // URL lookup hits on the first try.
112 $post_url = AllAccessible_UrlCanonicalizer::for_post((int) $post->ID);
113 if ($post_url === '') $post_url = (string) get_permalink($post->ID);
114
115 if ($post->post_status !== 'publish') {
116 ?>
117 <div class="aacb-metabox-wrapper allaccessible-admin aacx-v2">
118 <p class="aacb-notice"><?php _e('Publish this page to see its accessibility score.', 'allaccessible'); ?></p>
119 </div>
120 <?php
121 return;
122 }
123
124 if (!class_exists('AllAccessible_ApiClient')) {
125 return;
126 }
127
128 $client = AllAccessible_ApiClient::get_instance();
129 $audit = $client->get_page_audit((int) $post->ID, $post_url);
130
131 if (!is_wp_error($audit) && isset($audit['data_source']) && $audit['data_source'] === 'page') {
132 // Only attempt linkage when we actually got a page row.
133 $client->link_post_to_page((int) $post->ID, $post_url);
134 }
135
136 AllAccessible_Debug::console('EditorMetaBox — post ' . (int) $post->ID, array(
137 'post_id' => (int) $post->ID,
138 'raw_permalink' => (string) get_permalink($post->ID),
139 'canonical_url' => $post_url,
140 'api_is_error' => is_wp_error($audit),
141 'api_error' => is_wp_error($audit) ? $audit->get_error_message() : null,
142 'api_response' => is_wp_error($audit) ? null : $audit,
143 'render_branch' => (is_wp_error($audit) || !is_array($audit)) ? 'score_unavailable' : 'full_panel',
144 ));
145
146 if (is_wp_error($audit) || !is_array($audit)) {
147 ?>
148 <div class="aacb-metabox-wrapper allaccessible-admin aacx-v2">
149 <div class="aacx-v2__banner aacx-v2__banner--warn">
150 <div>
151 <strong><?php _e('Score unavailable', 'allaccessible'); ?></strong>
152 <p style="font-size: var(--aacx-text-xs); margin-top: var(--aacx-space-1);">
153 <?php _e('Could not load this page\'s accessibility score. Try refreshing the editor.', 'allaccessible'); ?>
154 </p>
155 </div>
156 </div>
157 </div>
158 <?php
159 return;
160 }
161
162 $score = isset($audit['overall_score']) ? (int) $audit['overall_score'] : null;
163 $issues = is_array($audit['issues'] ?? null) ? $audit['issues'] : array();
164 $total_issues = (int) ($audit['total_issues'] ?? 0);
165 $last_scan = isset($audit['last_scan']) ? (string) $audit['last_scan'] : '';
166 $data_source = (string) ($audit['data_source'] ?? 'none');
167 $audit_status = (string) ($audit['audit_status'] ?? 'never');
168 $breakdown = is_array($audit['score_breakdown'] ?? null) ? $audit['score_breakdown'] : array();
169 $score_raw = isset($breakdown['raw']) ? (int) $breakdown['raw'] : null;
170 $score_potential = isset($breakdown['potential']) ? (int) $breakdown['potential'] : null;
171 $widget_fixed = isset($breakdown['widget_fixed']) ? (int) $breakdown['widget_fixed'] : 0;
172 $manifest_active = isset($breakdown['manifest_approved']) ? (int) $breakdown['manifest_approved'] : 0;
173 $manifest_pending = isset($breakdown['manifest_pending']) ? (int) $breakdown['manifest_pending'] : 0;
174
175 // Score → color band + grade label.
176 $score_class = 'score-poor';
177 $grade_label = __('Needs attention', 'allaccessible');
178 if ($score === null) {
179 $score_class = '';
180 $grade_label = __('Awaiting scan', 'allaccessible');
181 } elseif ($score >= 90) {
182 $score_class = 'score-excellent';
183 $grade_label = __('Excellent', 'allaccessible');
184 } elseif ($score >= 75) {
185 $score_class = 'score-good';
186 $grade_label = __('Good', 'allaccessible');
187 } elseif ($score >= 50) {
188 $score_class = 'score-fair';
189 $grade_label = __('Needs review', 'allaccessible');
190 }
191
192 // Last-scan human-readable.
193 $last_scan_human = '';
194 if ($last_scan !== '') {
195 $ts = strtotime($last_scan);
196 if ($ts) {
197 $last_scan_human = sprintf(
198 /* translators: %s: human-readable time diff (e.g., "2 hours") */
199 __('%s ago', 'allaccessible'),
200 human_time_diff($ts, current_time('timestamp'))
201 );
202 }
203 }
204
205 $deeplink = 'https://app.allaccessible.org';
206 $score_deeplink = '';
207 $opts = $client->get_site_options();
208 $audit_id = isset($audit['audit_id']) ? (int) $audit['audit_id'] : 0;
209 $sub_id_api = isset($audit['subdomain_id']) ? (int) $audit['subdomain_id'] : 0;
210 if (!is_wp_error($opts) && is_object($opts) && !empty($opts->siteID)) {
211 $sub = $sub_id_api > 0 ? $sub_id_api : (int) get_option('aacb_siteID');
212 $tok = (string) $opts->siteID;
213 if ($sub > 0 && $tok !== '') {
214 $deeplink = sprintf(
215 'https://app.allaccessible.org/site/%s/%d/accessibility-audits',
216 rawurlencode($tok),
217 $sub
218 );
219 if ($audit_id > 0) {
220 $score_deeplink = sprintf(
221 'https://app.allaccessible.org/site/%s/%d/audit/%d',
222 rawurlencode($tok),
223 $sub,
224 $audit_id
225 );
226 }
227 }
228 }
229 ?>
230 <?php
231 $has_lift = ($score_raw !== null && $score !== null && $score_raw < $score);
232 $has_pending = ($score_potential !== null && $score !== null && $score_potential > $score);
233 $score_str = $score !== null ? (string) $score : '—';
234 $circle_aria = $score !== null
235 ? sprintf(__('Accessibility score %1$d out of 100, %2$s. Open full audit.', 'allaccessible'), (int) $score, $grade_label)
236 : __('Accessibility score not available yet.', 'allaccessible');
237 ?>
238 <div class="aacb-metabox-wrapper allaccessible-admin aacx-v2">
239
240 <!-- 1. Score circle — the visual anchor; metabox title supplies "Accessibility Score" -->
241 <div class="aacb-score-display">
242 <?php if ($score_deeplink !== '') : ?>
243 <a href="<?php echo esc_url($score_deeplink); ?>"
244 target="_blank"
245 rel="noopener"
246 class="aacb-score-circle <?php echo esc_attr($score_class); ?>"
247 aria-label="<?php echo esc_attr($circle_aria); ?>">
248 <div class="aacb-score-number"><?php echo esc_html($score_str); ?></div>
249 <div class="aacb-score-grade"><?php echo esc_html($grade_label); ?></div>
250 </a>
251 <?php else : ?>
252 <div class="aacb-score-circle <?php echo esc_attr($score_class); ?>"
253 aria-label="<?php echo esc_attr($circle_aria); ?>">
254 <div class="aacb-score-number"><?php echo esc_html($score_str); ?></div>
255 <div class="aacb-score-grade"><?php echo esc_html($grade_label); ?></div>
256 </div>
257 <?php endif; ?>
258 </div>
259
260 <!-- 2. Lift strip — raw → current delta with green +N gain pill -->
261 <?php if ($has_lift) :
262 $gain = (int) $score - (int) $score_raw;
263 $lift_aria = sprintf(
264 /* translators: 1: raw, 2: current, 3: lift delta */
265 __('Score lifted from %1$d to %2$d by AllAccessible AI, a gain of %3$d points.', 'allaccessible'),
266 (int) $score_raw, (int) $score, $gain
267 );
268 ?>
269 <div class="aacx-lift-strip" role="group" aria-label="<?php echo esc_attr($lift_aria); ?>">
270 <span class="aacx-v2__ai-badge">
271 <?php esc_html_e('AllAccessible AI', 'allaccessible'); ?>
272 </span>
273 <span class="aacx-lift-delta" aria-hidden="true">
274 <span class="aacx-lift-from"><?php echo esc_html((int) $score_raw); ?></span>
275 <span class="aacx-lift-arrow">→</span>
276 <span class="aacx-lift-to"><?php echo esc_html((int) $score); ?></span>
277 <span class="aacx-lift-gain">+<?php echo esc_html($gain); ?></span>
278 </span>
279 </div>
280 <?php
281
282 $detail_parts = array();
283 if ($widget_fixed > 0) {
284 $detail_parts[] = sprintf(
285 /* translators: %s: count of issues AllAccessible auto-resolves at runtime */
286 _n('%s issue resolved by AllAccessible', '%s issues resolved by AllAccessible', $widget_fixed, 'allaccessible'),
287 number_format_i18n($widget_fixed)
288 );
289 }
290 if ($manifest_active > 0) {
291 $detail_parts[] = sprintf(
292 /* translators: %s: count of live agentic AI fixes */
293 _n('%s agentic AI fix live', '%s agentic AI fixes live', $manifest_active, 'allaccessible'),
294 number_format_i18n($manifest_active)
295 );
296 }
297 if (!empty($detail_parts)) : ?>
298 <p class="aacx-lift-detail"><?php echo esc_html(implode(' · ', $detail_parts)); ?></p>
299 <?php endif; ?>
300 <?php endif; ?>
301
302 <!-- 3. Pending fixes CTA — full-card click target, only when pending lift available -->
303 <?php if ($has_pending) :
304 $pending_aria = sprintf(
305 /* translators: 1: pending count, 2: potential score */
306 _n(
307 'Review %1$d pending agentic AI fix; approve to reach %2$d%% accessibility score',
308 'Review %1$d pending agentic AI fixes; approve to reach %2$d%% accessibility score',
309 $manifest_pending,
310 'allaccessible'
311 ),
312 $manifest_pending, (int) $score_potential
313 );
314 ?>
315 <a href="<?php echo esc_url(admin_url('admin.php?page=aacb-agentic-fixes')); ?>"
316 class="aacx-pending-cta aacx-v2__card aacx-v2__card--ai"
317 aria-label="<?php echo esc_attr($pending_aria); ?>">
318 <div class="aacx-pending-cta__icon" aria-hidden="true">✦</div>
319 <div class="aacx-pending-cta__body">
320 <div class="aacx-pending-cta__title">
321 <?php printf(
322 /* translators: %s: count of pending fixes */
323 esc_html(_n('%s fix ready to review', '%s fixes ready to review', $manifest_pending, 'allaccessible')),
324 esc_html(number_format_i18n($manifest_pending))
325 ); ?>
326 </div>
327 <div class="aacx-pending-cta__sub">
328 <?php printf(
329 /* translators: %s: potential score */
330 esc_html__('Approve to reach %s%%', 'allaccessible'),
331 '<strong>' . esc_html((int) $score_potential) . '</strong>'
332 ); ?>
333 </div>
334 </div>
335 <div class="aacx-pending-cta__chev" aria-hidden="true">›</div>
336 </a>
337 <?php endif; ?>
338
339 <!-- 4. Issues summary — compact inline chips -->
340 <div class="aacx-issues-summary">
341 <div class="aacx-issues-summary__title">
342 <?php printf(
343 /* translators: %s: total issues */
344 esc_html__('Issues (%s)', 'allaccessible'),
345 esc_html(number_format_i18n($total_issues))
346 ); ?>
347 </div>
348 <div class="aacx-issues-chips" role="list">
349 <?php
350 $chip_specs = array(
351 'crit' => array('count' => (int) ($issues['critical'] ?? 0), 'label' => __('Critical', 'allaccessible')),
352 'ser' => array('count' => (int) ($issues['serious'] ?? 0), 'label' => __('Serious', 'allaccessible')),
353 'mod' => array('count' => (int) ($issues['moderate'] ?? 0), 'label' => __('Moderate', 'allaccessible')),
354 'min' => array('count' => (int) ($issues['minor'] ?? 0), 'label' => __('Minor', 'allaccessible')),
355 );
356 foreach ($chip_specs as $key => $spec) :
357 $chip_class = 'aacx-issue-chip aacx-issue-chip--' . $key . ($spec['count'] === 0 ? ' aacx-issue-chip--zero' : '');
358 $chip_aria = sprintf('%s: %d', $spec['label'], $spec['count']);
359 ?>
360 <span class="<?php echo esc_attr($chip_class); ?>"
361 role="listitem"
362 title="<?php echo esc_attr($spec['label']); ?>"
363 aria-label="<?php echo esc_attr($chip_aria); ?>">
364 <span class="aacx-issue-chip__dot" aria-hidden="true"></span>
365 <?php echo esc_html(number_format_i18n($spec['count'])); ?>
366 </span>
367 <?php endforeach; ?>
368 </div>
369 </div>
370
371 <!-- 5. Last scanned -->
372 <?php if ($last_scan_human !== '') : ?>
373 <div class="aacb-last-scan">
374 <span class="dashicons dashicons-clock" aria-hidden="true"></span>
375 <span><?php
376 printf(
377 /* translators: %s: human-readable time ago */
378 esc_html__('Last scanned %s', 'allaccessible'),
379 esc_html($last_scan_human)
380 );
381 ?></span>
382 </div>
383 <?php endif; ?>
384
385 <!-- 6. Primary CTA — full audit -->
386 <div class="aacb-actions" style="display:flex;flex-direction:column;gap:var(--aacx-space-2);">
387 <a href="<?php echo esc_url($score_deeplink !== '' ? $score_deeplink : $deeplink); ?>"
388 target="_blank"
389 rel="noopener"
390 class="aacx-v2__btn aacx-v2__btn--primary"
391 style="width: 100%;">
392 <?php _e('View full report', 'allaccessible'); ?>
393 <span aria-hidden="true">↗</span>
394 </a>
395 <button type="button"
396 class="aacx-v2__btn aacx-v2__btn--secondary aacb-metabox-rescan"
397 data-post-url="<?php echo esc_attr((string) get_permalink($post->ID)); ?>"
398 data-post-id="<?php echo esc_attr((int) $post->ID); ?>"
399 style="width: 100%;">
400 <?php _e('Rescan this page', 'allaccessible'); ?>
401 </button>
402 </div>
403
404 <style>
405 /* Minimal local spinner + status. Scoped to .aacb-metabox-rescan
406 so it doesn't fight wp-admin styles elsewhere. */
407 @keyframes aacb-rescan-spin { to { transform: rotate(360deg); } }
408 .aacb-metabox-rescan .dashicons-update.is-spinning {
409 animation: aacb-rescan-spin 1s linear infinite;
410 vertical-align: -3px;
411 margin-right: 4px;
412 }
413 .aacb-rescan-status {
414 display: block;
415 margin-top: 8px;
416 font-size: 12px;
417 line-height: 1.4;
418 }
419 .aacb-rescan-status.is-error { color: #b91c1c; }
420 .aacb-rescan-status.is-warning { color: #92400e; }
421 .aacb-rescan-status.is-ok { color: #15803d; }
422 .aacb-rescan-status .aacb-rescan-action {
423 margin-left: 6px;
424 text-decoration: underline;
425 cursor: pointer;
426 }
427 </style>
428 <script>
429 (function(){
430 if (typeof document === 'undefined') return;
431 document.addEventListener('DOMContentLoaded', function(){
432 var btn = document.querySelector('.aacb-metabox-rescan');
433 if (!btn) return;
434 var ajaxUrl = <?php echo wp_json_encode(admin_url('admin-ajax.php')); ?>;
435 var scanNonce = <?php echo wp_json_encode(wp_create_nonce('aacb_scan_this_page')); ?>;
436 var statusNonce = <?php echo wp_json_encode(wp_create_nonce('aacb_scan_status')); ?>;
437 // ONE label from click through done. Was two-stage
438 // ("queued" then "running") which read as redundant.
439 var i18n = {
440 scanning: <?php echo wp_json_encode(__('Scanning page…', 'allaccessible')); ?>,
441 doneClean: <?php echo wp_json_encode(__('Scan complete. Refreshing…', 'allaccessible')); ?>,
442 doneDirty: <?php echo wp_json_encode(__('Scan complete. Save or reload to see the updated score.', 'allaccessible')); ?>,
443 viewScore: <?php echo wp_json_encode(__('Reload now', 'allaccessible')); ?>,
444 failed: <?php echo wp_json_encode(__('Scan failed. Try again or check the dashboard.', 'allaccessible')); ?>,
445 timeout: <?php echo wp_json_encode(__('Still running — refresh the page in a minute.', 'allaccessible')); ?>,
446 rate: <?php echo wp_json_encode(__('Scan rate limit reached. Try again later.', 'allaccessible')); ?>,
447 error: <?php echo wp_json_encode(__('Could not start scan.', 'allaccessible')); ?>,
448 network: <?php echo wp_json_encode(__('Network error. Check your connection and try again.', 'allaccessible')); ?>,
449 };
450 var origLabel = btn.textContent;
451
452 // Inline status node beneath the button. Replaces the
453 // old alert() popups, which were jarring inside Gutenberg.
454 var statusEl = document.createElement('span');
455 statusEl.className = 'aacb-rescan-status';
456 statusEl.setAttribute('aria-live', 'polite');
457 statusEl.hidden = true;
458 btn.insertAdjacentElement('afterend', statusEl);
459
460 function setStatus(text, variant) {
461 statusEl.textContent = text || '';
462 statusEl.className = 'aacb-rescan-status' + (variant ? ' is-' + variant : '');
463 statusEl.hidden = !text;
464 }
465 function setButtonScanning() {
466 btn.disabled = true;
467 btn.innerHTML = '<span class="dashicons dashicons-update is-spinning" aria-hidden="true"></span>' + escapeHtml(i18n.scanning);
468 }
469 function resetButton() {
470 btn.disabled = false;
471 btn.textContent = origLabel;
472 }
473 function escapeHtml(s) {
474 var d = document.createElement('div');
475 d.textContent = s;
476 return d.innerHTML;
477 }
478 function isPostDirty() {
479 try {
480 // Gutenberg: ask the editor whether the post has
481 // unsaved edits. Auto-reload would lose them.
482 if (window.wp && wp.data && typeof wp.data.select === 'function') {
483 var ed = wp.data.select('core/editor');
484 if (ed && typeof ed.isEditedPostDirty === 'function') {
485 return !!ed.isEditedPostDirty();
486 }
487 }
488 } catch (e) {}
489 return false; // Classic editor: assume safe to reload.
490 }
491 function finishDone() {
492 if (isPostDirty()) {
493 resetButton();
494 statusEl.textContent = i18n.doneDirty + ' ';
495 statusEl.className = 'aacb-rescan-status is-ok';
496 statusEl.hidden = false;
497 var link = document.createElement('a');
498 link.href = '#';
499 link.className = 'aacb-rescan-action';
500 link.textContent = i18n.viewScore;
501 link.addEventListener('click', function(ev){
502 ev.preventDefault();
503 window.location.reload();
504 });
505 statusEl.appendChild(link);
506 return;
507 }
508 setStatus(i18n.doneClean, 'ok');
509 setTimeout(function(){ window.location.reload(); }, 2500);
510 }
511
512 btn.addEventListener('click', function(e){
513 e.preventDefault();
514 if (btn.disabled) return;
515 var pageUrl = btn.dataset.postUrl || '';
516 if (!pageUrl) return;
517
518 setStatus('', null);
519 setButtonScanning();
520
521 var fd = new FormData();
522 fd.append('action', 'aacb_scan_this_page');
523 fd.append('_ajax_nonce', scanNonce);
524 fd.append('page_url', pageUrl);
525
526 fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body:fd})
527 .then(function(r){ return r.json().catch(function(){ return null; }); })
528 .then(function(j){
529 if (j && j.success && j.data && j.data.jobId) {
530 pollStatus(j.data.jobId, pageUrl);
531 } else {
532 resetButton();
533 var msg = (j && j.data && j.data.message) ? j.data.message : i18n.error;
534 if (j && j.data && j.data.code === 429) msg = i18n.rate;
535 setStatus(msg, 'error');
536 }
537 })
538 .catch(function(){
539 resetButton();
540 setStatus(i18n.network, 'error');
541 });
542 });
543
544 function pollStatus(jobId, pageUrl) {
545 var started = Date.now();
546 var intervalMs = 4000;
547 var maxMs = 180000;
548 var timer = setInterval(function(){
549 if (Date.now() - started > maxMs) {
550 clearInterval(timer);
551 resetButton();
552 setStatus(i18n.timeout, 'warning');
553 return;
554 }
555 var fd = new FormData();
556 fd.append('action', 'aacb_scan_status');
557 fd.append('_ajax_nonce', statusNonce);
558 fd.append('job_id', jobId);
559 fd.append('page_url', pageUrl);
560 fetch(ajaxUrl, {method:'POST', credentials:'same-origin', body:fd})
561 .then(function(r){ return r.json().catch(function(){ return null; }); })
562 .then(function(j){
563 if (!j || !j.success || !j.data) return; // transient — keep polling
564 var status = j.data.status;
565 if (status === 'done') {
566 clearInterval(timer);
567 finishDone();
568 } else if (status === 'failed') {
569 clearInterval(timer);
570 resetButton();
571 setStatus(i18n.failed, 'error');
572 }
573 // queued / running: spinner already conveys
574 // progress — no need to flip labels.
575 })
576 .catch(function(){ /* transient — keep polling */ });
577 }, intervalMs);
578 }
579 });
580 })();
581 </script>
582
583 <!-- 8. Footer status -->
584 <div class="aacb-update-notice">
585 <?php if ($data_source === 'page') : ?>
586 <span class="dashicons dashicons-yes-alt" aria-hidden="true" style="color: var(--aacx-ok-600);"></span>
587 <?php _e('Page-level score', 'allaccessible'); ?>
588 <?php elseif ($data_source === 'site') : ?>
589 <span class="dashicons dashicons-info" aria-hidden="true"></span>
590 <?php _e('Site-level score (page not yet crawled)', 'allaccessible'); ?>
591 <?php else : ?>
592 <span class="dashicons dashicons-info" aria-hidden="true"></span>
593 <?php _e('Awaiting first scan', 'allaccessible'); ?>
594 <?php endif; ?>
595 </div>
596 </div>
597
598 <style>
599 .aacb-metabox-wrapper { padding: 10px; }
600
601 /* Score circle — anchor */
602 .aacb-score-display { text-align: center; margin-bottom: var(--aacx-space-3); }
603 .aacb-score-circle {
604 width: 100px;
605 height: 100px;
606 border-radius: 50%;
607 border: 7px solid #e5e7eb;
608 display: inline-flex;
609 flex-direction: column;
610 align-items: center;
611 justify-content: center;
612 margin: 0 auto;
613 text-decoration: none;
614 cursor: pointer;
615 transition: transform var(--aacx-transition);
616 }
617 a.aacb-score-circle:hover { transform: scale(1.02); }
618 .aacb-score-circle.score-excellent { border-color: #00aa62; }
619 .aacb-score-circle.score-good { border-color: #54b8ff; }
620 .aacb-score-circle.score-fair { border-color: #f59e0b; }
621 .aacb-score-circle.score-poor { border-color: #ef4444; }
622 .aacb-score-number {
623 font-size: 28px;
624 font-weight: var(--aacx-weight-bold);
625 color: var(--aacx-text-strong);
626 line-height: 1;
627 }
628 .aacb-score-grade {
629 font-size: var(--aacx-text-xs);
630 color: var(--aacx-text-muted);
631 margin-top: 2px;
632 }
633
634 /* Lift strip */
635 .aacx-lift-strip {
636 display: flex;
637 align-items: center;
638 gap: var(--aacx-space-2);
639 flex-wrap: wrap;
640 padding: var(--aacx-space-2) var(--aacx-space-3);
641 background: linear-gradient(135deg, var(--aacx-ai-50) 0%, var(--aacx-primary-50) 100%);
642 border: 1px solid var(--aacx-ai-200);
643 border-radius: var(--aacx-radius-md);
644 margin-bottom: var(--aacx-space-1);
645 }
646 .aacx-lift-delta {
647 display: inline-flex;
648 align-items: center;
649 gap: var(--aacx-space-2);
650 font-variant-numeric: tabular-nums;
651 }
652 .aacx-lift-from { color: var(--aacx-text-muted); font-size: var(--aacx-text-sm); }
653 .aacx-lift-arrow { color: var(--aacx-text-muted); font-size: var(--aacx-text-base); }
654 .aacx-lift-to {
655 color: var(--aacx-text-strong);
656 font-size: var(--aacx-text-sm);
657 font-weight: var(--aacx-weight-semibold);
658 }
659 .aacx-lift-gain {
660 display: inline-flex;
661 align-items: center;
662 padding: 0 var(--aacx-space-2);
663 height: 18px;
664 background: var(--aacx-ok-100);
665 color: var(--aacx-ok-700);
666 font-size: var(--aacx-text-xs);
667 font-weight: var(--aacx-weight-semibold);
668 border-radius: var(--aacx-radius-pill);
669 line-height: 1;
670 }
671 .aacx-lift-detail {
672 margin: var(--aacx-space-1) 0 var(--aacx-space-3);
673 font-size: var(--aacx-text-xs);
674 color: var(--aacx-text-muted);
675 line-height: var(--aacx-leading-normal);
676 }
677
678 /* Pending fixes CTA card */
679 .aacx-pending-cta {
680 display: flex;
681 align-items: center;
682 gap: var(--aacx-space-3);
683 padding: var(--aacx-space-3);
684 margin-bottom: var(--aacx-space-3);
685 text-decoration: none !important;
686 transition: border-color var(--aacx-transition), box-shadow var(--aacx-transition);
687 }
688 .aacx-pending-cta:hover {
689 border-color: var(--aacx-ai-300);
690 box-shadow: var(--aacx-shadow-sm);
691 }
692 .aacx-pending-cta__icon {
693 flex-shrink: 0;
694 width: 28px; height: 28px;
695 display: flex; align-items: center; justify-content: center;
696 background: var(--aacx-ai-100);
697 color: var(--aacx-ai-700);
698 border-radius: 50%;
699 font-size: var(--aacx-text-base);
700 font-weight: var(--aacx-weight-bold);
701 }
702 .aacx-pending-cta__body { flex: 1; min-width: 0; }
703 .aacx-pending-cta__title {
704 font-size: var(--aacx-text-sm);
705 font-weight: var(--aacx-weight-semibold);
706 color: var(--aacx-ai-800);
707 line-height: 1.3;
708 }
709 .aacx-pending-cta__sub {
710 font-size: var(--aacx-text-xs);
711 color: var(--aacx-text-muted);
712 margin-top: 2px;
713 line-height: 1.3;
714 }
715 .aacx-pending-cta__sub strong {
716 color: var(--aacx-text-strong);
717 font-weight: var(--aacx-weight-semibold);
718 }
719 .aacx-pending-cta__chev {
720 color: var(--aacx-ai-700);
721 font-size: var(--aacx-text-lg);
722 font-weight: var(--aacx-weight-bold);
723 line-height: 1;
724 }
725
726 /* Issue chips */
727 .aacx-issues-summary { margin-bottom: var(--aacx-space-3); }
728 .aacx-issues-summary__title {
729 font-size: var(--aacx-text-sm);
730 font-weight: var(--aacx-weight-semibold);
731 color: var(--aacx-text-strong);
732 margin-bottom: var(--aacx-space-2);
733 }
734 .aacx-issues-chips {
735 display: flex;
736 gap: var(--aacx-space-2);
737 flex-wrap: wrap;
738 }
739 .aacx-issue-chip {
740 display: inline-flex;
741 align-items: center;
742 gap: var(--aacx-space-1);
743 padding: 0 var(--aacx-space-2);
744 height: 22px;
745 background: var(--aacx-slate-100);
746 color: var(--aacx-text-strong);
747 font-size: var(--aacx-text-xs);
748 font-weight: var(--aacx-weight-semibold);
749 border-radius: var(--aacx-radius-pill);
750 font-variant-numeric: tabular-nums;
751 line-height: 1;
752 transition: background var(--aacx-transition);
753 }
754 .aacx-issue-chip:hover { background: var(--aacx-slate-200); }
755 .aacx-issue-chip--zero { opacity: 0.5; }
756 .aacx-issue-chip__dot {
757 width: 6px; height: 6px;
758 border-radius: 50%;
759 display: inline-block;
760 flex-shrink: 0;
761 }
762 .aacx-issue-chip--crit .aacx-issue-chip__dot { background: var(--aacx-danger-500); }
763 .aacx-issue-chip--ser .aacx-issue-chip__dot { background: var(--aacx-urgent-500); }
764 .aacx-issue-chip--mod .aacx-issue-chip__dot { background: var(--aacx-warn-500); }
765 .aacx-issue-chip--min .aacx-issue-chip__dot { background: var(--aacx-primary-500); }
766 .aacx-issue-chip--zero .aacx-issue-chip__dot { background: var(--aacx-slate-300); }
767
768 /* Last scanned line */
769 .aacb-last-scan {
770 display: flex;
771 align-items: center;
772 gap: var(--aacx-space-1);
773 margin: var(--aacx-space-3) 0;
774 font-size: var(--aacx-text-xs);
775 color: var(--aacx-text-muted);
776 }
777 .aacb-last-scan .dashicons {
778 font-size: 12px; width: 12px; height: 12px;
779 }
780
781 /* Primary CTA spacing */
782 .aacb-actions { margin-bottom: var(--aacx-space-3); }
783
784 /* Footer status */
785 .aacb-update-notice {
786 margin-top: var(--aacx-space-2);
787 padding-top: var(--aacx-space-2);
788 border-top: 1px solid var(--aacx-border);
789 text-align: center;
790 font-size: var(--aacx-text-xs);
791 color: var(--aacx-text-muted);
792 }
793 .aacb-update-notice .dashicons {
794 font-size: 14px; width: 14px; height: 14px;
795 vertical-align: middle;
796 }
797
798 /* Empty / loading states (unchanged from prior layout) */
799 .aacb-loading { text-align: center; padding: 20px; }
800 .aacb-notice {
801 padding: 12px;
802 background: #f0f6fc;
803 border-left: 4px solid #54b8ff;
804 margin: 0;
805 }
806 </style>
807 <?php
808 }
809
810 /**
811 * Render upgrade prompt for free users
812 */
813 public function render_upgrade_meta_box($post) {
814 ?>
815 <div class="aacb-metabox-wrapper allaccessible-admin aacx-v2">
816 <div class="aacb-upgrade-prompt" style="text-align: center; padding: var(--aacx-space-4) 0;">
817 <svg style="width: 48px; height: 48px; margin: 0 auto var(--aacx-space-3); color: var(--aacx-slate-400); display: block;" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
818 <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
819 </svg>
820 <h4 style="font-size: var(--aacx-text-lg); font-weight: var(--aacx-weight-semibold); margin-bottom: var(--aacx-space-2);">
821 <?php _e('Premium Feature', 'allaccessible'); ?>
822 </h4>
823 <p style="font-size: var(--aacx-text-sm); color: var(--aacx-text-muted); margin-bottom: var(--aacx-space-4);">
824 <?php _e('See accessibility scores for each page/post with a premium account.', 'allaccessible'); ?>
825 </p>
826 <a href="<?php echo admin_url('admin.php?page=allaccessible#pluginSettings'); ?>" class="aacx-v2__btn aacx-v2__btn--primary">
827 <?php _e('Upgrade to Premium', 'allaccessible'); ?>
828 </a>
829 <p style="font-size: var(--aacx-text-xs); color: var(--aacx-text-muted); margin-top: var(--aacx-space-3);">
830 <?php _e('7-day free trial available', 'allaccessible'); ?>
831 </p>
832 </div>
833 </div>
834 <?php
835 }
836 }
837
838 // Initialize editor meta box after WordPress is fully loaded
839 add_action('plugins_loaded', function() {
840 AllAccessible_EditorMetaBox::get_instance();
841 });
842