| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 66.67% covered (warning) | 66.67% | 82 / 123 | 36.84% covered (danger) | 36.84% | 7 / 19 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
Renderer | 66.67% covered (warning) | 66.67% | 82 / 123 | 36.84% covered (danger) | 36.84% | 7 / 19 | 80.37 | 0.00% covered (danger) | 0.00% | 0 / 1
pageHeader | 100.00% covered (success) | 100.00% | 5 / 5 | 100.00% covered (success) | 100.00% | 1 / 1 | 2 |
pageFooter | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
card | 100.00% covered (success) | 100.00% | 7 / 7 | 100.00% covered (success) | 100.00% | 1 / 1 | 3 |
notice | 100.00% covered (success) | 100.00% | 2 / 2 | 100.00% covered (success) | 100.00% | 1 / 1 | 2 |
textField | 91.67% covered (success) | 91.67% | 11 / 12 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.00 |
textareaField | 90.00% covered (success) | 90.00% | 9 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.00 |
selectField | 92.31% covered (success) | 92.31% | 12 / 13 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.00 |
checkboxField | 90.91% covered (success) | 90.91% | 10 / 11 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.00 |
radioField | 93.33% covered (success) | 93.33% | 14 / 15 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.00 |
formOpen | 100.00% covered (success) | 100.00% | 4 / 4 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
formClose | 100.00% covered (success) | 100.00% | 6 / 6 | 100.00% covered (success) | 100.00% | 1 / 1 | 2 |
formTableOpen | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
formTableClose | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
kvRow | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
noticeAccessible | 0.00% covered (danger) | 0.00% | 0 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
renderLoadingSkeleton | 0.00% covered (danger) | 0.00% | 0 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
renderEmptyState | 0.00% covered (danger) | 0.00% | 0 / 9 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
renderHelpIcon | 0.00% covered (danger) | 0.00% | 0 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
pageFooterWithHelpers | 0.00% covered (danger) | 0.00% | 0 / 12 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |

1 | <?php
2 | /**
3 | * HTML Renderer helper — the swap point for future UI changes.
4 | *
5 | * Today: returns HTML strings (uses WordPress core admin markup).
6 | * Tomorrow (Y1.5+): can be replaced with React/MUI render() calls
7 | * without touching any page-level business logic.
8 | *
9 | * All admin pages should call these helpers rather than emitting
10 | * raw HTML strings inline. This is the seam.
11 | *
12 | * @package SwapAds\Client\Admin
13 | * @since 1.0.0
14 | */
15 |
16 | declare(strict_types=1);
17 |
18 | namespace SwapAds\Client\Admin;
19 |
20 | /**
21 | * Class Renderer
22 | *
23 | * @since 1.0.0
24 | */
25 | final class Renderer
26 | {
27 | /**
28 | * Open a wrap container with h1.
29 | */
30 | public static function pageHeader(string $title, string $description = ''): string
31 | {
32 | $h = '<div class="wrap swapads-page">';
33 | $h .= '<h1>' . esc_html($title) . '</h1>';
34 | if ($description !== '') {
35 | $h .= '<p class="description">' . esc_html($description) . '</p>';
36 | }
37 | return $h;
38 | }
39 |
40 | /**
41 | * Close the wrap container.
42 | */
43 | public static function pageFooter(): string
44 | {
45 | return '</div>';
46 | }
47 |
48 | /**
49 | * Card container (styled via the .swapads-card CSS class).
50 | *
51 | * @param string $title h2 heading shown at top of card
52 | * @param string $body HTML body content (already escaped by caller)
53 | */
54 | public static function card(string $title, string $body, string $extraClass = ''): string
55 | {
56 | $cls = 'swapads-card' . ($extraClass !== '' ? ' ' . $extraClass : '');
57 | $h = '<div class="' . esc_attr($cls) . '">';
58 | if ($title !== '') {
59 | $h .= '<h2>' . esc_html($title) . '</h2>';
60 | }
61 | $h .= $body;
62 | $h .= '</div>';
63 | return $h;
64 | }
65 |
66 | /**
67 | * Notice (success/error/warning).
68 | */
69 | public static function notice(string $message, string $type = 'success'): string
70 | {
71 | $type = in_array($type, ['success', 'error', 'warning', 'info'], true) ? $type : 'info';
72 | return '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
73 | }
74 |
75 | /**
76 | * Label + text input field.
77 | */
78 | public static function textField(string $name, string $label, string $value = '', string $placeholder = '', string $helpText = ''): string
79 | {
80 | $h = '<tr>';
81 | $h .= '<th scope="row"><label for="' . esc_attr($name) . '">' . esc_html($label) . '</label></th>';
82 | $h .= '<td>';
83 | $h .= '<input type="text" id="' . esc_attr($name) . '" name="' . esc_attr($name) . '" '
84 | . 'value="' . esc_attr($value) . '" '
85 | . 'placeholder="' . esc_attr($placeholder) . '" '
86 | . 'class="regular-text">';
87 | if ($helpText !== '') {
88 | $h .= '<p class="description">' . esc_html($helpText) . '</p>';
89 | }
90 | $h .= '</td>';
91 | $h .= '</tr>';
92 | return $h;
93 | }
94 |
95 | /**
96 | * Label + textarea field.
97 | */
98 | public static function textareaField(string $name, string $label, string $value = '', int $rows = 4, string $helpText = ''): string
99 | {
100 | $h = '<tr>';
101 | $h .= '<th scope="row"><label for="' . esc_attr($name) . '">' . esc_html($label) . '</label></th>';
102 | $h .= '<td>';
103 | $h .= '<textarea id="' . esc_attr($name) . '" name="' . esc_attr($name) . '" rows="' . (int) $rows . '" '
104 | . 'class="large-text">' . esc_textarea($value) . '</textarea>';
105 | if ($helpText !== '') {
106 | $h .= '<p class="description">' . esc_html($helpText) . '</p>';
107 | }
108 | $h .= '</td>';
109 | $h .= '</tr>';
110 | return $h;
111 | }
112 |
113 | /**
114 | * Label + select dropdown.
115 | *
116 | * @param array<int, array{value: string, label: string}> $options
117 | */
118 | public static function selectField(string $name, string $label, array $options, string $selected = '', string $helpText = ''): string
119 | {
120 | $h = '<tr>';
121 | $h .= '<th scope="row"><label for="' . esc_attr($name) . '">' . esc_html($label) . '</label></th>';
122 | $h .= '<td>';
123 | $h .= '<select id="' . esc_attr($name) . '" name="' . esc_attr($name) . '">';
124 | foreach ($options as $opt) {
125 | $sel = selected($selected, $opt['value'], false);
126 | $h .= '<option value="' . esc_attr($opt['value']) . '" ' . $sel . '>' . esc_html($opt['label']) . '</option>';
127 | }
128 | $h .= '</select>';
129 | if ($helpText !== '') {
130 | $h .= '<p class="description">' . esc_html($helpText) . '</p>';
131 | }
132 | $h .= '</td>';
133 | $h .= '</tr>';
134 | return $h;
135 | }
136 |
137 | /**
138 | * Label + checkbox.
139 | */
140 | public static function checkboxField(string $name, string $label, bool $checked = false, string $helpText = ''): string
141 | {
142 | $h = '<tr>';
143 | $h .= '<th scope="row">' . esc_html($label) . '</th>';
144 | $h .= '<td>';
145 | $h .= '<label><input type="checkbox" name="' . esc_attr($name) . '" value="1" ' . checked($checked, true, false) . '> ';
146 | $h .= esc_html($label);
147 | $h .= '</label>';
148 | if ($helpText !== '') {
149 | $h .= '<p class="description">' . esc_html($helpText) . '</p>';
150 | }
151 | $h .= '</td>';
152 | $h .= '</tr>';
153 | return $h;
154 | }
155 |
156 | /**
157 | * Radio group.
158 | *
159 | * @param array<int, array{value: string, label: string}> $options
160 | */
161 | public static function radioField(string $name, string $label, array $options, string $selected = '', string $helpText = ''): string
162 | {
163 | $h = '<tr>';
164 | $h .= '<th scope="row">' . esc_html($label) . '</th>';
165 | $h .= '<td><fieldset>';
166 | foreach ($options as $opt) {
167 | $h .= '<label style="margin-right:12px;">';
168 | $h .= '<input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr($opt['value']) . '" '
169 | . checked($selected, $opt['value'], false) . '> ';
170 | $h .= esc_html($opt['label']);
171 | $h .= '</label>';
172 | }
173 | $h .= '</fieldset>';
174 | if ($helpText !== '') {
175 | $h .= '<p class="description">' . esc_html($helpText) . '</p>';
176 | }
177 | $h .= '</td>';
178 | $h .= '</tr>';
179 | return $h;
180 | }
181 |
182 | /**
183 | * Open a form tag pointing to admin-post.php.
184 | */
185 | public static function formOpen(string $action, string $nonceAction, string $submitLabel = 'Submit'): string
186 | {
187 | $h = '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
188 | $h .= wp_nonce_field($nonceAction, '_wpnonce', true, false);
189 | $h .= '<input type="hidden" name="action" value="' . esc_attr($action) . '">';
190 | return $h;
191 | }
192 |
193 | /**
194 | * Close form + emit submit button.
195 | */
196 | public static function formClose(string $submitLabel = 'Save Changes', string $cancelUrl = ''): string
197 | {
198 | $h = '</tbody></table>';
199 | if ($cancelUrl !== '') {
200 | $h .= '<a href="' . esc_url($cancelUrl) . '" class="button">' . esc_html__('Cancel', 'swapads-client') . '</a> ';
201 | }
202 | $h .= '<button type="submit" class="button button-primary">' . esc_html($submitLabel) . '</button>';
203 | $h .= '</form>';
204 | return $h;
205 | }
206 |
207 | /**
208 | * Open a form-table tbody.
209 | */
210 | public static function formTableOpen(): string
211 | {
212 | return '<table class="form-table"><tbody>';
213 | }
214 |
215 | /**
216 | * Close a form-table tbody.
217 | */
218 | public static function formTableClose(): string
219 | {
220 | return '</tbody>';
221 | }
222 |
223 | /**
224 | * Plain key-value row (read-only display).
225 | */
226 | public static function kvRow(string $label, string $value): string
227 | {
228 | return '<tr><th scope="row">' . esc_html($label) . '</th><td>' . $value . '</td></tr>';
229 | }
230 |
231 | /**
232 | * Accessible notice (Feedback + Accessibility principles).
233 | *
234 | * WP core `notice-*` classes are styled by WP admin. Adds `role="alert"` +
235 | * `aria-live="polite"` so screen readers announce the change without
236 | * interrupting the user (WCAG 2.1 AA: 4.1.3 Status Messages).
237 | *
238 | * Use for success/error/warning/info messages that should be announced.
239 | *
240 | * @param string $type 'success' | 'error' | 'warning' | 'info'
241 | */
242 | public static function noticeAccessible(string $message, string $type = 'success'): string
243 | {
244 | $type = in_array($type, ['success', 'error', 'warning', 'info'], true) ? $type : 'info';
245 | return '<div class="notice notice-' . esc_attr($type) . ' is-dismissible" '
246 | . 'role="alert" aria-live="polite">'
247 | . '<p>' . esc_html($message) . '</p>'
248 | . '</div>';
249 | }
250 |
251 | /**
252 | * Loading skeleton placeholder (Feedback principle).
253 | *
254 | * Whenever a page fetches data from the server, render this skeleton
255 | * first so the user sees visual progress within ~200ms (Performance
256 | * principle). The skeleton is replaced by real content once the fetch
257 | * returns. Reduces perceived latency and prevents layout shift.
258 | *
259 | * @param string $kind 'tiles' | 'table' | 'paragraph' | 'card'
260 | * @param int $rows Number of skeleton rows (default 3).
261 | */
262 | public static function renderLoadingSkeleton(string $kind = 'card', int $rows = 3): string
263 | {
264 | $h = '<div class="swapads-skeleton swapads-skeleton-' . esc_attr($kind) . '" aria-busy="true" aria-live="polite">';
265 | for ($i = 0; $i < $rows; $i++) {
266 | $h .= '<div class="swapads-skeleton-row"></div>';
267 | }
268 | $h .= '</div>';
269 | return $h;
270 | }
271 |
272 | /**
273 | * Empty-state block (per UX-SPEC §1.4 + Feedback principle).
274 | *
275 | * When a list/query has no rows, show a friendly empty state with a
276 | * clear next action (CTA). Avoids the "lonely Nothing found" dead-end
277 | * that operators hit today.
278 | *
279 | * @param string $title Bold heading (e.g. "No backlinks yet")
280 | * @param string $body Explanatory text below
281 | * @param string|null $ctaUrl Optional URL for the primary CTA
282 | * @param string|null $ctaLabel Optional CTA button label
283 | */
284 | public static function renderEmptyState(
285 | string $title,
286 | string $body,
287 | ?string $ctaUrl = null,
288 | ?string $ctaLabel = null
289 | ): string {
290 | $h = '<div class="swapads-empty-state" role="status">';
291 | $h .= '<div class="swapads-empty-icon" aria-hidden="true">📅</div>';
292 | $h .= '<h3 class="swapads-empty-title">' . esc_html($title) . '</h3>';
293 | $h .= '<p class="swapads-empty-body">' . esc_html($body) . '</p>';
294 | if ($ctaUrl !== null && $ctaLabel !== null) {
295 | $h .= '<a href="' . esc_url($ctaUrl) . '" class="button button-primary">'
296 | . esc_html($ctaLabel) . '</a>';
297 | }
298 | $h .= '</div>';
299 | return $h;
300 | }
301 |
302 | /**
303 | * Inline help icon with tooltip (Discovery principle).
304 | *
305 | * Renders a small "?" icon that reveals a tooltip on hover/focus. Uses
306 | * WP's built-in screen-reader-text pattern for SR-only labels.
307 | *
308 | * @param string $text Plain-text help shown in the tooltip.
309 | * @param string $id Unique DOM id (caller must ensure uniqueness).
310 | */
311 | public static function renderHelpIcon(string $text, string $id): string
312 | {
313 | return '<span class="swapads-help-icon" role="tooltip" id="' . esc_attr($id) . '">'
314 | . '?<span class="screen-reader-text">' . esc_html($text) . '</span>'
315 | . '</span>';
316 | }
317 |
318 | /**
319 | * Render the shared page footer (Consistency principle).
320 | *
321 | * Every page SHOULD call this in lieu of `Renderer::pageFooter()` so the
322 | * Help / Docs link is consistent across the plugin. Future global
323 | * footer chrome (version, build SHA, support links) lands here.
324 | *
325 | * @param string $extra HTML to inject before the closing wrap div (rare).
326 | */
327 | public static function pageFooterWithHelpers(string $extra = ''): string
328 | {
329 | $h = $extra;
330 | $h .= '<div class="swapads-page-footer" role="contentinfo">';
331 | $h .= '<p class="swapads-page-footer-meta">';
332 | $h .= '<a href="https://swapads.eu/docs/operator" target="_blank" rel="noopener">'
333 | . esc_html__('Help & Docs', 'swapads-client') . '</a>';
334 | $h .= ' Â· ';
335 | $h .= '<a href="https://swapads.eu/support" target="_blank" rel="noopener">'
336 | . esc_html__('Support', 'swapads-client') . '</a>';
337 | $h .= '</p>';
338 | $h .= '</div>';
339 | $h .= '</div>'; // close .swapads-page wrap
340 | return $h;
341 | }
342 | }