| 1 |
<?php |
| 2 |
/** |
| 3 |
* Dashboard Layout — Site Overview hero. |
| 4 |
* |
| 5 |
* Public helpers: |
| 6 |
* - aacb_render_overview_hero( $extras = [], $opts = [] ) The Site Overview hero. |
| 7 |
* - aacb_info_tip( $text ) Accessible CSS tooltip. |
| 8 |
* - aacb_ov_dial( $score, $band ) Inline SVG score dial. |
| 9 |
* |
| 10 |
* @package AllAccessible |
| 11 |
* @since 2.0.0 |
| 12 |
*/ |
| 13 |
|
| 14 |
if (!defined('ABSPATH')) { |
| 15 |
die('You are not allowed to call this page directly.'); |
| 16 |
} |
| 17 |
|
| 18 |
/** |
| 19 |
* Render the four-up dashboard hero stat row. |
| 20 |
* |
| 21 |
* Tiles: Site Score · Pages Audited · Pending Fixes · Plan Usage. |
| 22 |
* |
| 23 |
* @param string $account_tier free|trial|starter|legacy|enterprise|unknown |
| 24 |
* @param object $site_options /validate response — must expose ->usageSummary if usage data exists |
| 25 |
* @param array $extras optional data injected from the page: |
| 26 |
* - 'site_score' int|null 0-100 |
| 27 |
* - 'pages_scanned' int|null |
| 28 |
* - 'pending_fixes' int|null |
| 29 |
* - 'active_fixes' int|null |
| 30 |
* - 'fixes_url' string |
| 31 |
* - 'images_total' int|null live alt_text_data count |
| 32 |
* - 'images_ai_generated' int|null subset with AI alt text |
| 33 |
* - 'images_pending' int|null AI suggested, awaiting review |
| 34 |
*/ |
| 35 |
if (!function_exists('aacb_info_tip')) { |
| 36 |
/** |
| 37 |
* Tiny accessible "ⓘ" affordance for a stat/card label. |
| 38 |
* |
| 39 |
* @param string $text Short plain-text explanation (one or two sentences). |
| 40 |
*/ |
| 41 |
function aacb_info_tip($text) { |
| 42 |
$text = (string) $text; |
| 43 |
if ($text === '') { |
| 44 |
return ''; |
| 45 |
} |
| 46 |
return '<span class="aacb-info-tip" tabindex="0" role="note" aria-label="' . esc_attr($text) . '">' |
| 47 |
. 'i<span class="aacb-info-tip__bubble" aria-hidden="true">' . esc_html($text) . '</span>' |
| 48 |
. '</span>'; |
| 49 |
} |
| 50 |
} |
| 51 |
|
| 52 |
if (!function_exists('aacb_ov_dial')) { |
| 53 |
/** |
| 54 |
* Score dial for the Site Overview hero. |
| 55 |
*/ |
| 56 |
function aacb_ov_dial($score, $band) { |
| 57 |
$has = is_int($score); |
| 58 |
$val = $has ? max(0, min(100, $score)) : 0; |
| 59 |
$r = 40; $cx = 52; $cy = 52; |
| 60 |
$circ = 2 * M_PI * $r; |
| 61 |
$off = $circ * (1 - $val / 100); |
| 62 |
$colors = array( |
| 63 |
'ok' => '#16a34a', |
| 64 |
'primary' => '#2563eb', |
| 65 |
'warn' => '#d97706', |
| 66 |
'danger' => '#dc2626', |
| 67 |
'none' => '#94a3b8', |
| 68 |
); |
| 69 |
$stroke = isset($colors[$band]) ? $colors[$band] : $colors['primary']; |
| 70 |
ob_start(); |
| 71 |
?> |
| 72 |
<svg width="104" height="104" viewBox="0 0 104 104" role="img" |
| 73 |
aria-label="<?php echo esc_attr(sprintf( |
| 74 |
/* translators: %s: accessibility score 0-100 or em-dash */ |
| 75 |
__('Accessibility score %s out of 100', 'allaccessible'), |
| 76 |
$has ? (string) $val : '—' |
| 77 |
)); ?>"> |
| 78 |
<circle cx="<?php echo $cx; ?>" cy="<?php echo $cy; ?>" r="<?php echo $r; ?>" fill="none" stroke="#e2e8f0" stroke-width="8"/> |
| 79 |
<?php if ($has) : ?> |
| 80 |
<circle cx="<?php echo $cx; ?>" cy="<?php echo $cy; ?>" r="<?php echo $r; ?>" fill="none" |
| 81 |
stroke="<?php echo esc_attr($stroke); ?>" stroke-width="8" stroke-linecap="round" |
| 82 |
stroke-dasharray="<?php echo esc_attr($circ); ?>" |
| 83 |
stroke-dashoffset="<?php echo esc_attr($off); ?>" |
| 84 |
transform="rotate(-90 <?php echo $cx; ?> <?php echo $cy; ?>)"/> |
| 85 |
<?php endif; ?> |
| 86 |
<text x="<?php echo $cx; ?>" y="<?php echo $cy - 2; ?>" text-anchor="middle" dominant-baseline="central" |
| 87 |
font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="30" font-weight="700" |
| 88 |
fill="<?php echo esc_attr($stroke); ?>"><?php echo esc_html($has ? (string) $val : '—'); ?></text> |
| 89 |
<?php if ($has) : ?> |
| 90 |
<text x="<?php echo $cx; ?>" y="<?php echo $cy + 22; ?>" text-anchor="middle" dominant-baseline="central" |
| 91 |
font-family="var(--aacx-font-sans, sans-serif)" font-size="11" font-weight="600" |
| 92 |
letter-spacing="1" fill="#94a3b8">/ 100</text> |
| 93 |
<?php endif; ?> |
| 94 |
</svg> |
| 95 |
<?php |
| 96 |
return ob_get_clean(); |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
if (!function_exists('aacb_render_overview_hero')) { |
| 101 |
/** |
| 102 |
* Site Overview hero. |
| 103 |
* |
| 104 |
* @param array $extras Same hero context SettingsPage builds (site_score, |
| 105 |
* remediation_score, pages_scanned, pending_fixes, |
| 106 |
* manual_required_count, ai_resolved_count, |
| 107 |
* images_ai_generated, images_total, usage_bars, ...). |
| 108 |
* @param array $opts has_agentic(bool), preview_url, dashboard_url, |
| 109 |
* fixes_url, issues_anchor. |
| 110 |
*/ |
| 111 |
function aacb_render_overview_hero($extras, $opts = array()) { |
| 112 |
$extras = is_array($extras) ? $extras : array(); |
| 113 |
$opts = is_array($opts) ? $opts : array(); |
| 114 |
|
| 115 |
$score = (isset($extras['site_score']) && $extras['site_score'] !== null) |
| 116 |
? (int) round((float) $extras['site_score']) : null; |
| 117 |
$remediation = (isset($extras['remediation_score']) && $extras['remediation_score'] !== null) |
| 118 |
? (int) round((float) $extras['remediation_score']) : null; |
| 119 |
$is_live = !empty($extras['is_live_estimate']); |
| 120 |
$pages = isset($extras['pages_scanned']) ? (int) $extras['pages_scanned'] : null; |
| 121 |
$pending = (int) ($extras['pending_fixes'] ?? 0); |
| 122 |
$manual = (int) ($extras['manual_required_count'] ?? 0); |
| 123 |
$ai_res = (int) ($extras['ai_resolved_count'] ?? 0); |
| 124 |
$alt_ai = (int) ($extras['images_ai_generated'] ?? 0); |
| 125 |
$img_total = (int) ($extras['images_total'] ?? 0); |
| 126 |
|
| 127 |
$pv_cur = null; $pv_lim = null; |
| 128 |
if (!empty($extras['usage_bars']) && is_array($extras['usage_bars'])) { |
| 129 |
foreach ($extras['usage_bars'] as $bar) { |
| 130 |
if (is_array($bar) && isset($bar['limit'])) { // first bar = pageviews |
| 131 |
$pv_cur = (int) ($bar['current'] ?? 0); |
| 132 |
$pv_lim = (int) $bar['limit']; |
| 133 |
break; |
| 134 |
} |
| 135 |
} |
| 136 |
} |
| 137 |
|
| 138 |
$has_agentic = !empty($opts['has_agentic']); |
| 139 |
$preview_url = isset($opts['preview_url']) ? (string) $opts['preview_url'] : ''; |
| 140 |
$dashboard_url = isset($opts['dashboard_url']) ? (string) $opts['dashboard_url'] : ''; |
| 141 |
$fixes_url = isset($opts['fixes_url']) ? (string) $opts['fixes_url'] : admin_url('admin.php?page=aacb-agentic-fixes'); |
| 142 |
$issues_anchor = isset($opts['issues_anchor']) ? (string) $opts['issues_anchor'] : '#aacb-overview-issues'; |
| 143 |
$pages_url = isset($opts['pages_url']) ? (string) $opts['pages_url'] : ''; |
| 144 |
|
| 145 |
$sched = isset($opts['schedule']) && is_array($opts['schedule']) ? $opts['schedule'] : null; |
| 146 |
$cad_header = ''; $cad_freq = ''; $cad_last = ''; $cad_next = ''; |
| 147 |
if ($sched) { |
| 148 |
$freq = (string) ($sched['scanFrequency'] ?? ''); |
| 149 |
$last_at = $sched['lastScanAt'] ?? null; |
| 150 |
$next_at = $sched['nextScanAt'] ?? null; |
| 151 |
$freq_word = array( |
| 152 |
'daily' => __('Daily', 'allaccessible'), |
| 153 |
'weekly' => __('Weekly', 'allaccessible'), |
| 154 |
'monthly' => __('Monthly', 'allaccessible'), |
| 155 |
'never' => __('On-demand', 'allaccessible'), |
| 156 |
); |
| 157 |
if (isset($freq_word[$freq])) { |
| 158 |
$cad_freq = $freq_word[$freq]; |
| 159 |
} elseif (!empty($sched['scanFrequencyDays'])) { |
| 160 |
$cad_freq = sprintf( |
| 161 |
/* translators: %s: number of days between scans */ |
| 162 |
__('Every %s days', 'allaccessible'), |
| 163 |
number_format_i18n((int) $sched['scanFrequencyDays']) |
| 164 |
); |
| 165 |
} |
| 166 |
if ($cad_freq !== '' || $last_at) { |
| 167 |
// 'never' = no recurring schedule (e.g. free tier) — scans are |
| 168 |
// on-demand, so don't label them "Scheduled". |
| 169 |
$cad_header = ($freq === 'never') |
| 170 |
? __('Scans', 'allaccessible') |
| 171 |
: __('Scheduled scans', 'allaccessible'); |
| 172 |
$now = current_time('timestamp'); |
| 173 |
if ($last_at && ($lt = strtotime($last_at))) { |
| 174 |
$cad_last = sprintf( |
| 175 |
/* translators: %s: human time diff, e.g. "2 hours" */ |
| 176 |
__('%s ago', 'allaccessible'), |
| 177 |
human_time_diff($lt, $now) |
| 178 |
); |
| 179 |
} else { |
| 180 |
$cad_last = __('No scans yet', 'allaccessible'); |
| 181 |
} |
| 182 |
if ($freq !== 'never' && $next_at && ($nt = strtotime($next_at))) { |
| 183 |
$cad_next = $nt > $now |
| 184 |
? sprintf(/* translators: %s: human time diff */ __('in %s', 'allaccessible'), human_time_diff($now, $nt)) |
| 185 |
: __('due now', 'allaccessible'); |
| 186 |
} |
| 187 |
} |
| 188 |
} |
| 189 |
$has_cadence = ($cad_header !== ''); |
| 190 |
|
| 191 |
// Verdict + score band. |
| 192 |
if ($score === null) { |
| 193 |
$verdict = __('Run your first scan', 'allaccessible'); $band = 'none'; $accent = 'var(--aacx-slate-400)'; |
| 194 |
} elseif ($score >= 90) { |
| 195 |
// NOTE: never claim "compliant" / "compliance" — legal-exposure |
| 196 |
// phrasing (see legal language audit). Describe the score/coverage. |
| 197 |
$verdict = __('Excellent — strong accessibility coverage.', 'allaccessible'); $band = 'ok'; $accent = 'var(--aacx-ok-600)'; |
| 198 |
} elseif ($score >= 75) { |
| 199 |
$verdict = __('Good — a few fixes to go.', 'allaccessible'); $band = 'primary'; $accent = 'var(--aacx-primary-600)'; |
| 200 |
} elseif ($score >= 50) { |
| 201 |
$verdict = __('Needs attention.', 'allaccessible'); $band = 'warn'; $accent = 'var(--aacx-warn-600)'; |
| 202 |
} else { |
| 203 |
$verdict = __('Needs work — start with the top issues.', 'allaccessible'); $band = 'danger'; $accent = 'var(--aacx-danger-600)'; |
| 204 |
} |
| 205 |
|
| 206 |
// Sub-line (pre-escaped segments joined by a muted dot). |
| 207 |
$dot = ' <span style="color:var(--aacx-border);">·</span> '; |
| 208 |
if ($score === null) { |
| 209 |
$sub_html = esc_html__('Run a scan and AllAccessible agents will check every page for accessibility issues.', 'allaccessible'); |
| 210 |
} else { |
| 211 |
$bits = array(); |
| 212 |
if ($remediation !== null) { |
| 213 |
$bits[] = sprintf( |
| 214 |
/* translators: %s: percentage with <b> wrapper */ |
| 215 |
esc_html__('%s%% of issues remediated', 'allaccessible'), |
| 216 |
'<b>' . esc_html(number_format_i18n($remediation)) . '</b>' |
| 217 |
); |
| 218 |
} |
| 219 |
if ($pages !== null && $pages > 0) { |
| 220 |
$pg = sprintf( |
| 221 |
esc_html(_n('%s page audited', '%s pages audited', $pages, 'allaccessible')), |
| 222 |
'<b>' . esc_html(number_format_i18n($pages)) . '</b>' |
| 223 |
); |
| 224 |
if ($pages_url) { |
| 225 |
$pg .= ' <a href="' . esc_url($pages_url) . '"' |
| 226 |
. ' style="color:var(--aacx-primary-600);font-weight:var(--aacx-weight-semibold);white-space:nowrap;text-decoration:none;">' |
| 227 |
. esc_html__('View pages', 'allaccessible') . ' →</a>'; |
| 228 |
} |
| 229 |
$bits[] = $pg; |
| 230 |
} |
| 231 |
$sub_html = implode($dot, $bits); |
| 232 |
} |
| 233 |
|
| 234 |
// Smart action bar — single most important next step. |
| 235 |
$act_class = 'aacb-ov-action--clear'; $act_icon = '✓'; $act_text = ''; $act_btn = ''; |
| 236 |
if ($has_agentic && $pending > 0) { |
| 237 |
$act_class = 'aacb-ov-action--attention'; $act_icon = '⚡'; |
| 238 |
$act_text = sprintf( |
| 239 |
esc_html(_n('%s fix is ready for your review', '%s fixes are ready for your review', $pending, 'allaccessible')), |
| 240 |
'<b>' . esc_html(number_format_i18n($pending)) . '</b>' |
| 241 |
); |
| 242 |
$act_btn = '<a href="' . esc_url($fixes_url) . '" class="aacx-v2__btn aacx-v2__btn--primary aacx-v2__btn--sm">' |
| 243 |
. esc_html__('Review fixes', 'allaccessible') . ' →</a>'; |
| 244 |
} elseif ($manual > 0) { |
| 245 |
$act_class = 'aacb-ov-action--attention'; $act_icon = '⚠'; |
| 246 |
$act_text = sprintf( |
| 247 |
esc_html(_n('%s issue needs a manual fix', '%s issues need a manual fix', $manual, 'allaccessible')), |
| 248 |
'<b>' . esc_html(number_format_i18n($manual)) . '</b>' |
| 249 |
); |
| 250 |
$act_btn = '<a href="' . esc_url($issues_anchor) . '" class="aacx-v2__btn aacx-v2__btn--secondary aacx-v2__btn--sm">' |
| 251 |
. esc_html__('View issues', 'allaccessible') . ' ↓</a>'; |
| 252 |
} elseif ($score !== null) { |
| 253 |
$act_class = 'aacb-ov-action--clear'; $act_icon = '✓'; |
| 254 |
$act_text = esc_html__('You\'re all caught up — AllAccessible agents are monitoring your site.', 'allaccessible'); |
| 255 |
} else { |
| 256 |
$act_class = 'aacb-ov-action--attention'; $act_icon = '✦'; |
| 257 |
$act_text = esc_html__('No scan yet — run one to find and fix accessibility issues.', 'allaccessible'); |
| 258 |
$act_btn = '<a href="' . esc_url($fixes_url) . '" class="aacx-v2__btn aacx-v2__btn--primary aacx-v2__btn--sm">' |
| 259 |
. esc_html__('Get started', 'allaccessible') . ' →</a>'; |
| 260 |
} |
| 261 |
|
| 262 |
// Metric strip (proof) — only items we have data for. |
| 263 |
$metrics = array(); |
| 264 |
$metrics[] = '<span class="aacb-ov-metrics__item aacb-ov-metrics__ai">✦ <b>' . esc_html(number_format_i18n($ai_res)) . '</b> ' . esc_html__('AI-resolved', 'allaccessible') . '</span>'; |
| 265 |
$metrics[] = '<span class="aacb-ov-metrics__item"><b>' . esc_html(number_format_i18n($manual)) . '</b> ' . esc_html__('manual', 'allaccessible') . '</span>'; |
| 266 |
if ($img_total > 0) { |
| 267 |
$metrics[] = '<span class="aacb-ov-metrics__item"><b>' . esc_html(number_format_i18n($alt_ai)) . '</b> ' . esc_html__('images with alt text', 'allaccessible') . '</span>'; |
| 268 |
} |
| 269 |
if ($pv_lim) { |
| 270 |
$metrics[] = '<span class="aacb-ov-metrics__item"><b>' . esc_html(number_format_i18n((int) $pv_cur)) . '</b> / ' . esc_html(number_format_i18n($pv_lim)) . ' ' . esc_html__('pageviews', 'allaccessible') . '</span>'; |
| 271 |
} |
| 272 |
$metric_sep = '<span class="aacb-ov-metrics__sep" aria-hidden="true">·</span>'; |
| 273 |
?> |
| 274 |
<div class="aacb-ov" style="--aacb-ov-accent: <?php echo esc_attr($accent); ?>;"> |
| 275 |
|
| 276 |
<section class="aacb-ov-hero" aria-label="<?php esc_attr_e('Accessibility status', 'allaccessible'); ?>"> |
| 277 |
<div class="aacb-ov-hero__dial"><?php echo aacb_ov_dial($score, $band); // SVG, internally escaped // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div> |
| 278 |
<div class="aacb-ov-hero__body"> |
| 279 |
<p class="aacb-ov-hero__verdict"><?php echo esc_html($verdict); ?></p> |
| 280 |
<p class="aacb-ov-hero__sub"> |
| 281 |
<?php echo $sub_html; // pre-escaped segments // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> |
| 282 |
<?php if ($is_live) : ?><span class="aacb-ov-hero__live"><i></i><?php esc_html_e('Live · updates nightly', 'allaccessible'); ?></span><?php endif; ?> |
| 283 |
</p> |
| 284 |
<?php if ($preview_url || $dashboard_url) : ?> |
| 285 |
<div class="aacb-ov-hero__actions" style="display:flex;gap:var(--aacx-space-2);margin-top:var(--aacx-space-4);flex-wrap:wrap;"> |
| 286 |
<?php if ($preview_url) : ?> |
| 287 |
<a href="<?php echo esc_url($preview_url); ?>" target="_blank" rel="noopener" class="aacx-v2__btn aacx-v2__btn--secondary aacx-v2__btn--sm"><?php esc_html_e('Preview widget', 'allaccessible'); ?></a> |
| 288 |
<?php endif; ?> |
| 289 |
<?php if ($dashboard_url) : ?> |
| 290 |
<a href="<?php echo esc_url($dashboard_url); ?>" target="_blank" rel="noopener" class="aacx-v2__btn aacx-v2__btn--ghost aacx-v2__btn--sm"><?php esc_html_e('Open dashboard', 'allaccessible'); ?> →</a> |
| 291 |
<?php endif; ?> |
| 292 |
</div> |
| 293 |
<?php endif; ?> |
| 294 |
</div> |
| 295 |
|
| 296 |
<?php if ($has_cadence) : ?> |
| 297 |
<div class="aacb-ov-hero__cadence" aria-label="<?php esc_attr_e('Scan schedule', 'allaccessible'); ?>"> |
| 298 |
<div class="aacb-ov-hero__cadence-label"> |
| 299 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> |
| 300 |
<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/> |
| 301 |
</svg> |
| 302 |
<?php echo esc_html($cad_header); ?> |
| 303 |
<?php echo aacb_info_tip(__('How often AllAccessible automatically scans your whole site behind the scenes for a deep accessibility analysis. You can also run on-demand scans of individual pages anytime — from the WordPress page editor, or on the front end while logged in.', 'allaccessible')); ?> |
| 304 |
</div> |
| 305 |
<?php if ($cad_freq !== '') : ?> |
| 306 |
<div class="aacb-ov-hero__cadence-row"><span><?php esc_html_e('Frequency', 'allaccessible'); ?></span><b><?php echo esc_html($cad_freq); ?></b></div> |
| 307 |
<?php endif; ?> |
| 308 |
<?php if ($cad_last !== '') : ?> |
| 309 |
<div class="aacb-ov-hero__cadence-row"><span><?php esc_html_e('Last', 'allaccessible'); ?></span><b><?php echo esc_html($cad_last); ?></b></div> |
| 310 |
<?php endif; ?> |
| 311 |
<?php if ($cad_next !== '') : ?> |
| 312 |
<div class="aacb-ov-hero__cadence-row"><span><?php esc_html_e('Next', 'allaccessible'); ?></span><b><?php echo esc_html($cad_next); ?></b></div> |
| 313 |
<?php endif; ?> |
| 314 |
</div> |
| 315 |
<?php endif; ?> |
| 316 |
</section> |
| 317 |
|
| 318 |
<div class="aacb-ov-action <?php echo esc_attr($act_class); ?>" role="status"> |
| 319 |
<span class="aacb-ov-action__text"> |
| 320 |
<span class="aacb-ov-action__icon" aria-hidden="true"><?php echo esc_html($act_icon); ?></span> |
| 321 |
<span><?php echo $act_text; // pre-escaped // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span> |
| 322 |
</span> |
| 323 |
<?php if ($act_btn) : ?><span><?php echo $act_btn; // pre-built, escaped // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span><?php endif; ?> |
| 324 |
</div> |
| 325 |
|
| 326 |
<div class="aacb-ov-metrics" aria-label="<?php esc_attr_e('Remediation activity', 'allaccessible'); ?>"> |
| 327 |
<?php echo implode(' ' . $metric_sep . ' ', $metrics); // pre-escaped // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> |
| 328 |
</div> |
| 329 |
|
| 330 |
</div> |
| 331 |
<?php |
| 332 |
} |
| 333 |
} |
| 334 |
|