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 / DashboardLayout.php

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

334 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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