@diegovelasquezweb/a11y-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/assets/discovery/crawler-config.json +11 -0
- package/assets/discovery/stack-detection.json +33 -0
- package/assets/remediation/axe-check-maps.json +31 -0
- package/assets/remediation/code-patterns.json +109 -0
- package/assets/remediation/guardrails.json +24 -0
- package/assets/remediation/intelligence.json +4166 -0
- package/assets/remediation/source-boundaries.json +46 -0
- package/assets/reporting/compliance-config.json +173 -0
- package/assets/reporting/manual-checks.json +944 -0
- package/assets/reporting/wcag-reference.json +588 -0
- package/package.json +37 -0
- package/scripts/audit.mjs +326 -0
- package/scripts/core/asset-loader.mjs +54 -0
- package/scripts/core/toolchain.mjs +102 -0
- package/scripts/core/utils.mjs +105 -0
- package/scripts/engine/analyzer.mjs +1022 -0
- package/scripts/engine/dom-scanner.mjs +685 -0
- package/scripts/engine/source-scanner.mjs +300 -0
- package/scripts/reports/builders/checklist.mjs +307 -0
- package/scripts/reports/builders/html.mjs +766 -0
- package/scripts/reports/builders/md.mjs +96 -0
- package/scripts/reports/builders/pdf.mjs +259 -0
- package/scripts/reports/renderers/findings.mjs +188 -0
- package/scripts/reports/renderers/html.mjs +452 -0
- package/scripts/reports/renderers/md.mjs +595 -0
- package/scripts/reports/renderers/pdf.mjs +551 -0
- package/scripts/reports/renderers/utils.mjs +42 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file report-html.mjs
|
|
3
|
+
* @description Generates a high-fidelity, interactive HTML accessibility audit report.
|
|
4
|
+
* It processes the audit findings, calculates compliance scores, and applies
|
|
5
|
+
* a premium design system with persona-based impact analysis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { log, readJson, getInternalPath, DEFAULTS } from "../../core/utils.mjs";
|
|
9
|
+
import { ASSET_PATHS, loadAssetJson } from "../../core/asset-loader.mjs";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
normalizeFindings,
|
|
14
|
+
buildSummary,
|
|
15
|
+
computeComplianceScore,
|
|
16
|
+
scoreLabel,
|
|
17
|
+
buildPersonaSummary,
|
|
18
|
+
wcagOverallStatus,
|
|
19
|
+
} from "../renderers/findings.mjs";
|
|
20
|
+
import { escapeHtml } from "../renderers/utils.mjs";
|
|
21
|
+
|
|
22
|
+
const RULE_METADATA = loadAssetJson(
|
|
23
|
+
ASSET_PATHS.reporting.wcagReference,
|
|
24
|
+
"assets/reporting/wcag-reference.json",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
buildIssueCard,
|
|
29
|
+
buildPageGroupedSection,
|
|
30
|
+
} from "../renderers/html.mjs";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Prints the CLI usage instructions and available options for the HTML report builder.
|
|
34
|
+
*/
|
|
35
|
+
function printUsage() {
|
|
36
|
+
log.info(`Usage:
|
|
37
|
+
node report-html.mjs [options]
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--input <path> Findings JSON path (default: .audit/a11y-findings.json)
|
|
41
|
+
--output <path> Output HTML path (required)
|
|
42
|
+
--target <text> Compliance target label (default: WCAG 2.2 AA)
|
|
43
|
+
-h, --help Show this help
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parses command-line arguments into a structured configuration object for the HTML builder.
|
|
49
|
+
* @param {string[]} argv - Array of command-line arguments.
|
|
50
|
+
* @returns {Object} A configuration object containing input/output paths and target settings.
|
|
51
|
+
*/
|
|
52
|
+
function parseArgs(argv) {
|
|
53
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
54
|
+
printUsage();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const args = {
|
|
59
|
+
input: getInternalPath("a11y-findings.json"),
|
|
60
|
+
output: "",
|
|
61
|
+
baseUrl: "",
|
|
62
|
+
target: DEFAULTS.complianceTarget,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
66
|
+
const key = argv[i];
|
|
67
|
+
const value = argv[i + 1];
|
|
68
|
+
if (!key.startsWith("--") || value === undefined) continue;
|
|
69
|
+
|
|
70
|
+
if (key === "--input") args.input = value;
|
|
71
|
+
if (key === "--output") args.output = value;
|
|
72
|
+
if (key === "--base-url") args.baseUrl = value;
|
|
73
|
+
if (key === "--target") args.target = value;
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return args;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Constructs the final HTML string for the accessibility report.
|
|
82
|
+
* @param {Object} args - The parsed CLI arguments.
|
|
83
|
+
* @param {Object[]} findings - The normalized list of audit findings.
|
|
84
|
+
* @param {Object} [metadata={}] - Optional metadata from the scan (e.g., date, target).
|
|
85
|
+
* @returns {string} The complete HTML document as a string.
|
|
86
|
+
*/
|
|
87
|
+
function buildHtml(args, findings, metadata = {}) {
|
|
88
|
+
const totals = buildSummary(findings);
|
|
89
|
+
const dateStr = new Date().toLocaleDateString("en-US", {
|
|
90
|
+
year: "numeric",
|
|
91
|
+
month: "long",
|
|
92
|
+
day: "numeric",
|
|
93
|
+
hour: "2-digit",
|
|
94
|
+
minute: "2-digit",
|
|
95
|
+
});
|
|
96
|
+
const hasIssues = findings.length > 0;
|
|
97
|
+
|
|
98
|
+
let siteHostname = args.baseUrl;
|
|
99
|
+
try {
|
|
100
|
+
/** @type {URL} */
|
|
101
|
+
const urlObj = new URL(
|
|
102
|
+
args.baseUrl.startsWith("http")
|
|
103
|
+
? args.baseUrl
|
|
104
|
+
: `https://${args.baseUrl}`,
|
|
105
|
+
);
|
|
106
|
+
siteHostname = urlObj.hostname;
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
109
|
+
const statusColor = hasIssues
|
|
110
|
+
? "text-rose-700 bg-rose-50 border-rose-200"
|
|
111
|
+
: "text-emerald-600 bg-emerald-50 border-emerald-200";
|
|
112
|
+
const statusIcon = hasIssues
|
|
113
|
+
? `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>`
|
|
114
|
+
: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`;
|
|
115
|
+
const statusText = hasIssues ? "WCAG Violations Found" : "Audit Passed";
|
|
116
|
+
|
|
117
|
+
/** @type {Object<string, number>} */
|
|
118
|
+
const _pageCounts = {};
|
|
119
|
+
for (const f of findings) {
|
|
120
|
+
_pageCounts[f.area] = (_pageCounts[f.area] || 0) + 1;
|
|
121
|
+
}
|
|
122
|
+
const _sortedPages = Object.entries(_pageCounts).sort((a, b) => b[1] - a[1]);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Tailwind classes for the select inputs.
|
|
126
|
+
* @type {string}
|
|
127
|
+
*/
|
|
128
|
+
const selectClasses =
|
|
129
|
+
"pl-4 pr-10 py-3 bg-white border border-slate-300 rounded-2xl text-sm font-bold text-slate-800 focus:outline-none focus:ring-4 focus:ring-[var(--primary)]/10 focus:border-[var(--primary)] shadow-sm transition-all appearance-none cursor-pointer relative bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20stroke%3D%22%23374151%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%221.5%22%3E%3Cpath%20d%3D%22m6%208%204%204%204-4%22%2F%3E%3C%2Fsvg%3E')] bg-[length:1.25rem_1.25rem] bg-[right_0.5rem_center] bg-no-repeat";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* HTML markup for the page filter dropdown.
|
|
133
|
+
* @type {string}
|
|
134
|
+
*/
|
|
135
|
+
const pageSelectHtml =
|
|
136
|
+
_sortedPages.length > 1
|
|
137
|
+
? `<div id="page-select-container" style="display:none" class="flex items-center gap-2 shrink-0">
|
|
138
|
+
<label for="page-select" class="text-xs font-bold text-slate-600 uppercase tracking-widest hidden sm:block whitespace-nowrap">Page:</label>
|
|
139
|
+
<select id="page-select" onchange="filterByPage(this.value)" class="${selectClasses}">
|
|
140
|
+
<option value="all">All pages (${_sortedPages.length})</option>
|
|
141
|
+
${_sortedPages.map(([pg, cnt]) => `<option value="${escapeHtml(pg)}">${escapeHtml(pg)} (${cnt})</option>`).join("")}
|
|
142
|
+
</select>
|
|
143
|
+
</div>`
|
|
144
|
+
: "";
|
|
145
|
+
|
|
146
|
+
const score = computeComplianceScore(totals);
|
|
147
|
+
const label = scoreLabel(score);
|
|
148
|
+
const wcagStatus = wcagOverallStatus(totals);
|
|
149
|
+
/**
|
|
150
|
+
* The hue value for the compliance score gauge (green for high, orange for medium, red for low).
|
|
151
|
+
* @type {number}
|
|
152
|
+
*/
|
|
153
|
+
const scoreHue = wcagStatus === "Fail" ? 0 : score >= 75 ? 142 : score >= 55 ? 38 : 0;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Summary of issues grouped by user persona impact.
|
|
157
|
+
* @type {Object}
|
|
158
|
+
*/
|
|
159
|
+
const personaCounts = buildPersonaSummary(findings);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Configuration for the persona impact matrix UI.
|
|
163
|
+
* @type {Object}
|
|
164
|
+
*/
|
|
165
|
+
const personaGroups = {};
|
|
166
|
+
for (const [key, cfg] of Object.entries(RULE_METADATA.personaConfig)) {
|
|
167
|
+
personaGroups[key] = { label: cfg.label, count: personaCounts[key] || 0, icon: cfg.icon };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const quickWins = findings
|
|
171
|
+
.filter(
|
|
172
|
+
(f) => (f.severity === "Critical" || f.severity === "Serious") && f.fixCode,
|
|
173
|
+
)
|
|
174
|
+
.slice(0, 3);
|
|
175
|
+
|
|
176
|
+
return `<!doctype html>
|
|
177
|
+
<html lang="en">
|
|
178
|
+
<head>
|
|
179
|
+
<meta charset="utf-8">
|
|
180
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
181
|
+
<title>Web Accessibility Audit — ${escapeHtml(siteHostname)}</title>
|
|
182
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
183
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
184
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
185
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
186
|
+
<style>
|
|
187
|
+
:root {
|
|
188
|
+
--primary-h: 226;
|
|
189
|
+
--primary-s: 70%;
|
|
190
|
+
--primary-l: 50%;
|
|
191
|
+
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
|
|
192
|
+
--primary-light: hsl(var(--primary-h), var(--primary-s), 95%);
|
|
193
|
+
--primary-dark: hsl(var(--primary-h), var(--primary-s), 30%);
|
|
194
|
+
--slate-50: #f8fafc;
|
|
195
|
+
--slate-100: #f1f5f9;
|
|
196
|
+
--slate-200: #e2e8f0;
|
|
197
|
+
--slate-300: #cbd5e1;
|
|
198
|
+
--slate-400: #94a3b8;
|
|
199
|
+
--slate-500: #64748b;
|
|
200
|
+
--slate-600: #475569;
|
|
201
|
+
--slate-700: #334155;
|
|
202
|
+
--slate-800: #1e293b;
|
|
203
|
+
--slate-900: #0f172a;
|
|
204
|
+
}
|
|
205
|
+
html { scroll-padding-top: 80px; }
|
|
206
|
+
body { background-color: var(--slate-50); font-family: 'Outfit', 'Inter', sans-serif; -webkit-font-smoothing: antialiased; letter-spacing: -0.011em; }
|
|
207
|
+
.glass-header { background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(12px) saturate(180%); -webkit-backdrop-filter: blur(12px) saturate(180%); }
|
|
208
|
+
.premium-card { background: white; border: 1px solid var(--slate-200); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
209
|
+
.premium-card:hover { transform: translateY(-2px); border-color: var(--slate-300); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04); }
|
|
210
|
+
.issue-card { position: relative; transition: all 0.3s ease; }
|
|
211
|
+
.issue-card::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; border-radius: 4px 0 0 4px; opacity: 0.8; }
|
|
212
|
+
.issue-card[data-severity="Critical"]::before { background: #e11d48; }
|
|
213
|
+
.issue-card[data-severity="Serious"]::before { background: #f97316; }
|
|
214
|
+
.issue-card[data-severity="Moderate"]::before { background: #f59e0b; }
|
|
215
|
+
.issue-card[data-severity="Minor"]::before { background: #10b981; }
|
|
216
|
+
|
|
217
|
+
.score-gauge { transform: rotate(-90deg); }
|
|
218
|
+
.score-gauge-bg { fill: none; stroke: var(--slate-100); stroke-width: 3; }
|
|
219
|
+
.score-gauge-val { fill: none; stroke-width: 3; stroke-linecap: round; transition: stroke-dasharray 1s ease-out; }
|
|
220
|
+
</style>
|
|
221
|
+
<script type="application/ld+json">
|
|
222
|
+
${JSON.stringify(
|
|
223
|
+
{
|
|
224
|
+
"@context": "https://schema.org",
|
|
225
|
+
"@type": "Report",
|
|
226
|
+
name: "Web Accessibility Audit Report",
|
|
227
|
+
about: args.baseUrl || siteHostname,
|
|
228
|
+
dateCreated: metadata.scanDate || new Date().toISOString(),
|
|
229
|
+
hasPart: findings.map((f) => ({
|
|
230
|
+
"@type": "CreativeWork",
|
|
231
|
+
identifier: f.id,
|
|
232
|
+
name: f.title,
|
|
233
|
+
description: f.impact,
|
|
234
|
+
url: f.url,
|
|
235
|
+
keywords: [f.severity, f.wcag, f.ruleId].filter(Boolean).join(", "),
|
|
236
|
+
})),
|
|
237
|
+
},
|
|
238
|
+
null,
|
|
239
|
+
2,
|
|
240
|
+
)}
|
|
241
|
+
</script>
|
|
242
|
+
</head>
|
|
243
|
+
<body class="text-slate-900 min-h-screen">
|
|
244
|
+
|
|
245
|
+
<header class="fixed top-0 left-0 right-0 z-50 glass-header border-b border-slate-200/80 shadow-sm" id="navbar">
|
|
246
|
+
<nav aria-label="Report header">
|
|
247
|
+
<div class="max-w-7xl mx-auto px-4 h-16 flex justify-between items-center">
|
|
248
|
+
<div class="flex items-center gap-3">
|
|
249
|
+
<div class="px-3 h-10 rounded-lg bg-slate-900 text-white font-bold text-base font-mono flex items-center justify-center shadow-md">a11y</div>
|
|
250
|
+
<h1 class="text-xl font-bold">Web Accessibility <span class="text-slate-500">Audit</span></h1>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="flex items-center gap-2 px-3 py-1.5 rounded-full border ${statusColor}">
|
|
253
|
+
${statusIcon} <span>${statusText}</span>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</nav>
|
|
257
|
+
</header>
|
|
258
|
+
|
|
259
|
+
<main id="main-content" class="max-w-7xl mx-auto px-4 pt-24 pb-20">
|
|
260
|
+
<div class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
261
|
+
<div>
|
|
262
|
+
<h2 class="text-3xl font-extrabold mb-2">Web Accessibility Audit</h2>
|
|
263
|
+
<p class="text-slate-500">${dateStr} • ${escapeHtml(args.baseUrl)}</p>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- Dashboard Grid -->
|
|
268
|
+
<div class="grid grid-cols-1 xl:grid-cols-12 gap-6 mb-12">
|
|
269
|
+
<!-- 1. Score & Main Stats -->
|
|
270
|
+
<div class="xl:col-span-8 grid grid-cols-1 md:grid-cols-12 gap-6">
|
|
271
|
+
<div class="md:col-span-5 premium-card rounded-2xl p-6 flex flex-col items-center justify-center text-center relative overflow-hidden">
|
|
272
|
+
<div class="absolute top-0 right-0 p-4 opacity-5">
|
|
273
|
+
<svg class="w-32 h-32 text-slate-900" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="relative w-32 h-32 mb-4">
|
|
276
|
+
<svg class="w-full h-full score-gauge" viewBox="0 0 36 36">
|
|
277
|
+
<circle class="score-gauge-bg" cx="18" cy="18" r="16" />
|
|
278
|
+
<circle class="score-gauge-val" cx="18" cy="18" r="16"
|
|
279
|
+
stroke="hsl(${scoreHue}, 70%, 50%)"
|
|
280
|
+
stroke-dasharray="${score}, 100" />
|
|
281
|
+
</svg>
|
|
282
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
|
283
|
+
<span class="text-4xl font-extrabold text-slate-900">${score}</span>
|
|
284
|
+
<span class="text-[10px] font-bold text-slate-600 uppercase tracking-tighter">Score</span>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
<h3 class="text-xl font-bold text-slate-900 mb-1">${label} Compliance</h3>
|
|
288
|
+
<p class="text-xs font-medium text-slate-500 max-w-[200px] leading-snug">Automated testing coverage based on ${escapeHtml(args.target)} technical checks.</p>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="md:col-span-7 grid grid-cols-2 gap-4">
|
|
292
|
+
<div class="premium-card p-5 rounded-2xl border-l-[6px] border-rose-500">
|
|
293
|
+
<div class="flex justify-between items-start mb-2">
|
|
294
|
+
<span class="text-[10px] font-bold text-rose-600 uppercase tracking-widest">Critical</span>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="text-4xl font-black text-slate-900">${totals.Critical}</div>
|
|
297
|
+
<p class="text-[10px] text-slate-600 font-medium mt-1 leading-tight">Functional blockers</p>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="premium-card p-5 rounded-2xl border-l-[6px] border-orange-500">
|
|
300
|
+
<div class="flex justify-between items-start mb-2">
|
|
301
|
+
<span class="text-[10px] font-bold text-orange-700 uppercase tracking-widest">Serious</span>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="text-4xl font-black text-slate-900">${totals.Serious}</div>
|
|
304
|
+
<p class="text-[10px] text-slate-600 font-medium mt-1 leading-tight">Serious impediments</p>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="premium-card p-5 rounded-2xl border-l-[6px] border-amber-400">
|
|
307
|
+
<div class="flex justify-between items-start mb-2">
|
|
308
|
+
<span class="text-[10px] font-bold text-amber-700 uppercase tracking-widest">Moderate</span>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="text-4xl font-black text-slate-900">${totals.Moderate}</div>
|
|
311
|
+
<p class="text-[10px] text-slate-600 font-medium mt-1 leading-tight">Significant friction</p>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="premium-card p-5 rounded-2xl border-l-[6px] border-emerald-500">
|
|
314
|
+
<div class="flex justify-between items-start mb-2">
|
|
315
|
+
<span class="text-[10px] font-bold text-emerald-700 uppercase tracking-widest">Minor</span>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="text-4xl font-black text-slate-900">${totals.Minor}</div>
|
|
318
|
+
<p class="text-[10px] text-slate-600 font-medium mt-1 leading-tight">Minor violations</p>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<!-- 2. Persona Impact -->
|
|
324
|
+
<div class="xl:col-span-4 premium-card rounded-2xl p-6">
|
|
325
|
+
<h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest mb-6 flex items-center gap-2">
|
|
326
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
327
|
+
Persona Impact Matrix
|
|
328
|
+
</h3>
|
|
329
|
+
<p class="text-xs text-slate-600 mb-6 -mt-4 leading-relaxed italic">Distribution of unique accessibility barriers per user profile. Duplicate errors are grouped to show strategic impact.</p>
|
|
330
|
+
<div class="space-y-4">
|
|
331
|
+
${Object.entries(personaGroups)
|
|
332
|
+
.map(
|
|
333
|
+
([, p]) => `
|
|
334
|
+
<div class="group">
|
|
335
|
+
<div class="flex items-center justify-between mb-2">
|
|
336
|
+
<div class="flex items-center gap-3">
|
|
337
|
+
<div class="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center text-slate-600 group-hover:bg-[var(--primary-light)] group-hover:text-[var(--primary)] transition-colors">${p.icon}</div>
|
|
338
|
+
<span class="text-sm font-bold text-slate-700">${p.label}</span>
|
|
339
|
+
</div>
|
|
340
|
+
<span class="text-xs font-black text-slate-900">${p.count} issues</span>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="w-full bg-slate-100 rounded-full h-1.5 overflow-hidden">
|
|
343
|
+
<div class="bg-[var(--primary)] h-full rounded-full transition-all duration-500" style="width: ${findings.length > 0 ? (p.count / findings.length) * 100 : 0}%"></div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>`,
|
|
346
|
+
)
|
|
347
|
+
.join("")}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<!-- Quick Wins Banner (Actionable Insight) -->
|
|
353
|
+
${
|
|
354
|
+
quickWins.length > 0
|
|
355
|
+
? `
|
|
356
|
+
<div class="premium-card rounded-2xl bg-slate-900 p-6 mb-12 relative overflow-hidden">
|
|
357
|
+
<div class="absolute -right-4 -bottom-4 opacity-10">
|
|
358
|
+
<svg class="w-32 h-32 text-[var(--primary)]" fill="currentColor" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="relative z-10">
|
|
361
|
+
<div class="flex items-center gap-3 mb-4">
|
|
362
|
+
<span class="px-2 py-0.5 rounded bg-[var(--primary)] text-[10px] font-black text-white uppercase tracking-tighter">AI Analysis</span>
|
|
363
|
+
<h3 class="text-xl font-bold text-white">Recommended Quick Wins</h3>
|
|
364
|
+
</div>
|
|
365
|
+
<p class="text-xs text-[var(--primary)]/80 mb-6 -mt-2 leading-relaxed italic">High-priority issues with ready-to-use code fixes for immediate remediation.</p>
|
|
366
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
367
|
+
${quickWins
|
|
368
|
+
.map(
|
|
369
|
+
(w) => `
|
|
370
|
+
<div class="bg-slate-800/50 border border-slate-700 p-4 rounded-xl backdrop-blur-sm">
|
|
371
|
+
<div class="flex items-center justify-between mb-2">
|
|
372
|
+
<span class="px-1.5 py-0.5 rounded bg-rose-500/20 text-rose-400 text-[9px] font-bold uppercase tracking-tight line-clamp-1">${w.severity}</span>
|
|
373
|
+
<span class="text-slate-300 text-[9px] font-mono">${w.id}</span>
|
|
374
|
+
</div>
|
|
375
|
+
<h4 class="text-sm font-bold text-slate-200 mb-1 line-clamp-1">${w.title}</h4>
|
|
376
|
+
<p class="text-[10px] text-slate-300 font-mono mb-3 truncate">Page: ${w.area}</p>
|
|
377
|
+
<button onclick="scrollToIssue('${w.id}')" class="text-[10px] font-bold text-blue-300 hover:text-blue-200 transition-colors uppercase tracking-widest flex items-center gap-1">
|
|
378
|
+
View Solution
|
|
379
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M17 8l4 4m0 0l-4 4m4-4H3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
380
|
+
</button>
|
|
381
|
+
</div>`,
|
|
382
|
+
)
|
|
383
|
+
.join("")}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>`
|
|
387
|
+
: ""
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
<div id="findings-toolbar" class="sticky top-16 z-40 bg-slate-50/95 backdrop-blur-md py-5 border-b border-slate-200/80 mb-8 flex flex-col gap-5">
|
|
391
|
+
<!-- Row 1: Title, View Toggle & Expand All -->
|
|
392
|
+
<div class="flex items-center justify-between w-full">
|
|
393
|
+
<div class="flex items-center gap-4">
|
|
394
|
+
<h3 class="text-xl font-extrabold text-slate-900 tracking-tight">Findings <span class="text-slate-600 font-bold ml-1">${findings.length}</span></h3>
|
|
395
|
+
<div class="flex gap-1 bg-white border border-slate-200 rounded-xl p-1 shadow-sm">
|
|
396
|
+
<button onclick="setView('severity')" id="view-severity" class="view-btn px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-widest bg-[var(--primary-light)] text-[var(--primary)] transition-all">By Severity</button>
|
|
397
|
+
${
|
|
398
|
+
Object.keys(_pageCounts).length > 1
|
|
399
|
+
? `<button onclick="setView('page')" id="view-page" class="view-btn px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-slate-700 transition-all">By Page</button>`
|
|
400
|
+
: ""
|
|
401
|
+
}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
<button onclick="toggleAllCards()" id="expand-all-btn" class="px-5 py-2 rounded-xl border border-slate-200 bg-white text-xs font-bold uppercase tracking-widest text-slate-600 hover:border-slate-300 hover:text-slate-800 shadow-sm transition-all">Expand all</button>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<!-- Row 2: Search & Filter Select -->
|
|
408
|
+
<div class="flex items-center gap-4 w-full">
|
|
409
|
+
<div class="relative flex-1">
|
|
410
|
+
<input type="text" id="search-input" oninput="handleSearch(this.value)" placeholder="Search violations..." class="w-full pl-11 pr-4 py-3 bg-white border border-slate-200 rounded-2xl text-sm font-medium focus:outline-none focus:ring-4 focus:ring-[var(--primary)]/10 focus:border-[var(--primary)] transition-all shadow-sm">
|
|
411
|
+
<svg class="absolute left-4 top-3.5 w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<div id="filter-controls" class="flex items-center gap-2 shrink-0">
|
|
415
|
+
<label for="filter-select" class="text-xs font-bold text-slate-600 uppercase tracking-widest hidden sm:block whitespace-nowrap">Filter by:</label>
|
|
416
|
+
<select id="filter-select" onchange="filterIssues(this.value)" class="${selectClasses}">
|
|
417
|
+
<optgroup label="General">
|
|
418
|
+
<option value="all">All Issues</option>
|
|
419
|
+
</optgroup>
|
|
420
|
+
<optgroup label="Severity">
|
|
421
|
+
<option value="Critical">Critical</option>
|
|
422
|
+
<option value="Serious">Serious</option>
|
|
423
|
+
<option value="Moderate">Moderate</option>
|
|
424
|
+
<option value="Minor">Minor</option>
|
|
425
|
+
</optgroup>
|
|
426
|
+
<optgroup label="WCAG Principle">
|
|
427
|
+
<option value="Perceivable">Perceivable</option>
|
|
428
|
+
<option value="Operable">Operable</option>
|
|
429
|
+
<option value="Understandable">Understandable</option>
|
|
430
|
+
<option value="Robust">Robust</option>
|
|
431
|
+
</optgroup>
|
|
432
|
+
</select>
|
|
433
|
+
</div>
|
|
434
|
+
${pageSelectHtml}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<div id="issues-container" class="space-y-6">
|
|
439
|
+
${findings.length === 0 ? "No issues found." : findings.map((f) => buildIssueCard(f)).join("\n")}
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<div id="page-container" style="display:none">
|
|
443
|
+
${buildPageGroupedSection(findings)}
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<footer class="mt-10 py-6 border-t border-slate-200 text-center">
|
|
447
|
+
<p class="text-slate-600 text-sm font-medium">Generated by <a href="https://github.com/diegovelasquezweb/a11y" target="_blank" class="text-slate-700 hover:text-[var(--primary)] font-semibold transition-colors">a11y</a> • <span class="text-slate-700">${escapeHtml(args.target)}</span></p>
|
|
448
|
+
</footer>
|
|
449
|
+
</main>
|
|
450
|
+
|
|
451
|
+
<script>
|
|
452
|
+
function setView(view) {
|
|
453
|
+
const severityContainer = document.getElementById('issues-container');
|
|
454
|
+
const pageContainer = document.getElementById('page-container');
|
|
455
|
+
const filterControls = document.getElementById('filter-controls');
|
|
456
|
+
const pageSelectCont = document.getElementById('page-select-container');
|
|
457
|
+
const btnSeverity = document.getElementById('view-severity');
|
|
458
|
+
const btnPage = document.getElementById('view-page');
|
|
459
|
+
|
|
460
|
+
if (view === 'severity') {
|
|
461
|
+
severityContainer.style.display = '';
|
|
462
|
+
pageContainer.style.display = 'none';
|
|
463
|
+
if (filterControls) filterControls.style.display = 'flex';
|
|
464
|
+
if (pageSelectCont) pageSelectCont.style.display = 'none';
|
|
465
|
+
|
|
466
|
+
if (btnSeverity) {
|
|
467
|
+
btnSeverity.classList.add('bg-[var(--primary-light)]', 'text-[var(--primary)]');
|
|
468
|
+
btnSeverity.classList.remove('text-slate-500', 'hover:text-slate-700');
|
|
469
|
+
}
|
|
470
|
+
if (btnPage) {
|
|
471
|
+
btnPage.classList.remove('bg-[var(--primary-light)]', 'text-[var(--primary)]');
|
|
472
|
+
btnPage.classList.add('text-slate-500', 'hover:text-slate-700');
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
severityContainer.style.display = 'none';
|
|
476
|
+
pageContainer.style.display = '';
|
|
477
|
+
if (filterControls) filterControls.style.display = 'flex';
|
|
478
|
+
if (pageSelectCont) {
|
|
479
|
+
pageSelectCont.style.display = 'flex';
|
|
480
|
+
const ps = document.getElementById('page-select');
|
|
481
|
+
if (ps) { ps.value = 'all'; filterByPage('all'); }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (btnPage) {
|
|
485
|
+
btnPage.classList.add('bg-[var(--primary-light)]', 'text-[var(--primary)]');
|
|
486
|
+
btnPage.classList.remove('text-slate-500', 'hover:text-slate-700');
|
|
487
|
+
}
|
|
488
|
+
if (btnSeverity) {
|
|
489
|
+
btnSeverity.classList.remove('bg-[var(--primary-light)]', 'text-[var(--primary)]');
|
|
490
|
+
btnSeverity.classList.add('text-slate-500', 'hover:text-slate-700');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
_syncExpandBtn();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function _activeCards() {
|
|
497
|
+
const sev = document.getElementById('issues-container');
|
|
498
|
+
const pg = document.getElementById('page-container');
|
|
499
|
+
const container = sev && sev.style.display !== 'none' ? sev : pg;
|
|
500
|
+
return container ? [...container.querySelectorAll('.issue-card')] : [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function _syncExpandBtn() {
|
|
504
|
+
const btn = document.getElementById('expand-all-btn');
|
|
505
|
+
if (!btn) return;
|
|
506
|
+
const anyCollapsed = _activeCards().some(c => c.dataset.collapsed === 'true');
|
|
507
|
+
btn.textContent = anyCollapsed ? 'Expand all' : 'Collapse all';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function switchIssueTab(button, tabKey) {
|
|
511
|
+
const root = button.closest('[data-issue-tab-root]');
|
|
512
|
+
if (!root) return;
|
|
513
|
+
const card = button.closest('.issue-card');
|
|
514
|
+
if (!card) return;
|
|
515
|
+
|
|
516
|
+
const tabs = [...root.querySelectorAll('[role="tab"]')];
|
|
517
|
+
const panels = [...card.querySelectorAll('[role="tabpanel"][data-tab-panel]')];
|
|
518
|
+
|
|
519
|
+
tabs.forEach((tab) => {
|
|
520
|
+
const isActive = tab === button;
|
|
521
|
+
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
522
|
+
tab.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
523
|
+
if (isActive) {
|
|
524
|
+
tab.classList.add('bg-white', 'text-indigo-700', 'border', 'border-indigo-200', 'shadow-sm');
|
|
525
|
+
tab.classList.remove('text-slate-600', 'border-transparent', 'hover:bg-white/70');
|
|
526
|
+
} else {
|
|
527
|
+
tab.classList.remove('bg-white', 'text-indigo-700', 'border', 'border-indigo-200', 'shadow-sm');
|
|
528
|
+
tab.classList.add('text-slate-600', 'border-transparent', 'hover:bg-white/70');
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
panels.forEach((panel) => {
|
|
533
|
+
const isTarget = panel.dataset.tabPanel === tabKey;
|
|
534
|
+
panel.classList.toggle('hidden', !isTarget);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function handleIssueTabKeydown(event, button) {
|
|
539
|
+
const root = button.closest('[data-issue-tab-root]');
|
|
540
|
+
if (!root) return;
|
|
541
|
+
const tabs = [...root.querySelectorAll('[role="tab"]')];
|
|
542
|
+
const index = tabs.indexOf(button);
|
|
543
|
+
if (index === -1) return;
|
|
544
|
+
|
|
545
|
+
let nextIndex = index;
|
|
546
|
+
if (event.key === 'ArrowRight') nextIndex = (index + 1) % tabs.length;
|
|
547
|
+
else if (event.key === 'ArrowLeft') nextIndex = (index - 1 + tabs.length) % tabs.length;
|
|
548
|
+
else if (event.key === 'Home') nextIndex = 0;
|
|
549
|
+
else if (event.key === 'End') nextIndex = tabs.length - 1;
|
|
550
|
+
else if (event.key === 'Enter' || event.key === ' ') {
|
|
551
|
+
event.preventDefault();
|
|
552
|
+
const key = button.getAttribute('aria-controls')?.split('-').pop();
|
|
553
|
+
if (key) switchIssueTab(button, key);
|
|
554
|
+
return;
|
|
555
|
+
} else {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
event.preventDefault();
|
|
560
|
+
tabs[nextIndex].focus();
|
|
561
|
+
const key = tabs[nextIndex].getAttribute('aria-controls')?.split('-').pop();
|
|
562
|
+
if (key) switchIssueTab(tabs[nextIndex], key);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
function toggleCard(header) {
|
|
568
|
+
const card = header.closest('.issue-card, .manual-card');
|
|
569
|
+
const body = card.querySelector('.card-body');
|
|
570
|
+
const chevron = header.querySelector('.card-chevron');
|
|
571
|
+
const isCollapsed = card.dataset.collapsed === 'true';
|
|
572
|
+
if (isCollapsed) {
|
|
573
|
+
body.style.gridTemplateRows = '1fr';
|
|
574
|
+
card.dataset.collapsed = 'false';
|
|
575
|
+
if (chevron) chevron.style.transform = 'rotate(180deg)';
|
|
576
|
+
} else {
|
|
577
|
+
body.style.gridTemplateRows = '0fr';
|
|
578
|
+
card.dataset.collapsed = 'true';
|
|
579
|
+
if (chevron) chevron.style.transform = '';
|
|
580
|
+
}
|
|
581
|
+
header.setAttribute('aria-expanded', isCollapsed ? 'true' : 'false');
|
|
582
|
+
_syncExpandBtn();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function toggleAllCards() {
|
|
586
|
+
const cards = _activeCards();
|
|
587
|
+
const anyCollapsed = cards.some(c => c.dataset.collapsed === 'true');
|
|
588
|
+
cards.forEach(card => {
|
|
589
|
+
const body = card.querySelector('.card-body');
|
|
590
|
+
const chevron = card.querySelector('.card-chevron');
|
|
591
|
+
if (anyCollapsed) {
|
|
592
|
+
body.style.gridTemplateRows = '1fr';
|
|
593
|
+
card.dataset.collapsed = 'false';
|
|
594
|
+
if (chevron) chevron.style.transform = 'rotate(180deg)';
|
|
595
|
+
} else {
|
|
596
|
+
body.style.gridTemplateRows = '0fr';
|
|
597
|
+
card.dataset.collapsed = 'true';
|
|
598
|
+
if (chevron) chevron.style.transform = '';
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
const btn = document.getElementById('expand-all-btn');
|
|
602
|
+
if (btn) btn.textContent = anyCollapsed ? 'Collapse all' : 'Expand all';
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function filterByPage(page) {
|
|
606
|
+
document.querySelectorAll('.page-group').forEach(group => {
|
|
607
|
+
group.style.display = (page === 'all' || group.dataset.page === page) ? '' : 'none';
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function filterIssues(type) {
|
|
612
|
+
const cards = document.querySelectorAll('.issue-card');
|
|
613
|
+
const q = document.getElementById('search-input')?.value.toLowerCase().trim() || '';
|
|
614
|
+
let visibleCount = 0;
|
|
615
|
+
const principles = ['Perceivable', 'Operable', 'Understandable', 'Robust'];
|
|
616
|
+
let emptyMsg = document.getElementById('dynamic-empty-state');
|
|
617
|
+
const container = document.getElementById('issues-container');
|
|
618
|
+
|
|
619
|
+
cards.forEach(card => {
|
|
620
|
+
const severity = card.dataset.severity;
|
|
621
|
+
let match = false;
|
|
622
|
+
if (type === 'all') match = true;
|
|
623
|
+
else if (severity === type) match = true;
|
|
624
|
+
if (principles.includes(type)) {
|
|
625
|
+
const badge = card.querySelector('.wcag-label');
|
|
626
|
+
const wcagText = badge ? badge.textContent : '';
|
|
627
|
+
if (type === 'Perceivable' && wcagText.includes(' 1.')) match = true;
|
|
628
|
+
if (type === 'Operable' && wcagText.includes(' 2.')) match = true;
|
|
629
|
+
if (type === 'Understandable' && wcagText.includes(' 3.')) match = true;
|
|
630
|
+
if (type === 'Robust' && wcagText.includes(' 4.')) match = true;
|
|
631
|
+
}
|
|
632
|
+
const searchableText = [...card.querySelectorAll('.searchable-field')]
|
|
633
|
+
.map(el => el.textContent).join(' ').concat(' ', card.id).toLowerCase();
|
|
634
|
+
const textMatch = !q || searchableText.includes(q);
|
|
635
|
+
if (match && textMatch) { card.style.display = ''; visibleCount++; }
|
|
636
|
+
else { card.style.display = 'none'; }
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
if (visibleCount === 0) {
|
|
640
|
+
if (!emptyMsg) {
|
|
641
|
+
emptyMsg = document.createElement('div');
|
|
642
|
+
emptyMsg.id = 'dynamic-empty-state';
|
|
643
|
+
emptyMsg.className = 'text-center py-12 bg-slate-50 rounded-xl border border-slate-100 border-dashed';
|
|
644
|
+
emptyMsg.innerHTML = '<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 mb-4 text-slate-400"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg></div><h3 class="text-sm font-bold text-slate-900">No matching issues</h3><p class="text-xs text-slate-500 mt-1 uppercase tracking-widest font-bold">No violations found for this filter</p>';
|
|
645
|
+
container.appendChild(emptyMsg);
|
|
646
|
+
}
|
|
647
|
+
emptyMsg.style.display = 'block';
|
|
648
|
+
} else {
|
|
649
|
+
if (emptyMsg) emptyMsg.style.display = 'none';
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
window.addEventListener('scroll', () => {
|
|
654
|
+
const nav = document.getElementById('navbar');
|
|
655
|
+
if (window.scrollY > 20) {
|
|
656
|
+
nav.classList.add('shadow-md');
|
|
657
|
+
} else {
|
|
658
|
+
nav.classList.remove('shadow-md');
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
function scrollToIssue(id) {
|
|
663
|
+
const el = document.getElementById(id);
|
|
664
|
+
if (!el) return;
|
|
665
|
+
|
|
666
|
+
setView('severity');
|
|
667
|
+
filterIssues('all');
|
|
668
|
+
|
|
669
|
+
const body = el.querySelector('.card-body');
|
|
670
|
+
const chevron = el.querySelector('.card-chevron');
|
|
671
|
+
if (el.dataset.collapsed === 'true') {
|
|
672
|
+
body.style.gridTemplateRows = '1fr';
|
|
673
|
+
el.dataset.collapsed = 'false';
|
|
674
|
+
if (chevron) chevron.style.transform = 'rotate(180deg)';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const navHeight = 64 + (document.getElementById('findings-toolbar')?.offsetHeight || 0);
|
|
678
|
+
window.scrollTo({
|
|
679
|
+
top: el.offsetTop - navHeight - 20,
|
|
680
|
+
behavior: 'smooth'
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
el.classList.add('ring-4', 'ring-[var(--primary)]/30', 'border-[var(--primary)]');
|
|
684
|
+
setTimeout(() => {
|
|
685
|
+
el.classList.remove('ring-4', 'ring-[var(--primary)]/30', 'border-[var(--primary)]');
|
|
686
|
+
}, 2000);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function handleSearch(query) {
|
|
690
|
+
const filterType = document.getElementById('filter-select')?.value || 'all';
|
|
691
|
+
filterIssues(filterType);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function copyToClipboard(text, btn) {
|
|
695
|
+
const original = btn.innerHTML;
|
|
696
|
+
try {
|
|
697
|
+
await navigator.clipboard.writeText(text);
|
|
698
|
+
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
|
|
699
|
+
btn.classList.add('bg-emerald-500');
|
|
700
|
+
setTimeout(() => {
|
|
701
|
+
btn.innerHTML = original;
|
|
702
|
+
btn.classList.remove('bg-emerald-500');
|
|
703
|
+
}, 2000);
|
|
704
|
+
} catch {
|
|
705
|
+
btn.textContent = 'Failed';
|
|
706
|
+
btn.classList.add('bg-red-500');
|
|
707
|
+
setTimeout(() => {
|
|
708
|
+
btn.innerHTML = original;
|
|
709
|
+
btn.classList.remove('bg-red-500');
|
|
710
|
+
}, 2000);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
</script>
|
|
714
|
+
</body>
|
|
715
|
+
</html>`;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* The main execution function for the HTML report builder.
|
|
720
|
+
* Coordinates data reading, normalization, HTML generation, and file writing.
|
|
721
|
+
* @throws {Error} If required arguments are missing or input data is invalid.
|
|
722
|
+
*/
|
|
723
|
+
function main() {
|
|
724
|
+
const args = parseArgs(process.argv.slice(2));
|
|
725
|
+
if (!args.output) {
|
|
726
|
+
throw new Error("Missing required --output flag for HTML report location.");
|
|
727
|
+
}
|
|
728
|
+
const inputPayload = readJson(args.input);
|
|
729
|
+
if (!inputPayload) {
|
|
730
|
+
throw new Error(`Input findings file not found or invalid: ${args.input}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const findings = normalizeFindings(inputPayload).filter(
|
|
734
|
+
(f) => f.wcagClassification !== "AAA" && f.wcagClassification !== "Best Practice",
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const screenshotsDir = getInternalPath("screenshots");
|
|
738
|
+
for (const finding of findings) {
|
|
739
|
+
if (finding.screenshotPath) {
|
|
740
|
+
const filename = path.basename(finding.screenshotPath);
|
|
741
|
+
const absolutePath = path.join(screenshotsDir, filename);
|
|
742
|
+
if (fs.existsSync(absolutePath)) {
|
|
743
|
+
const data = fs.readFileSync(absolutePath);
|
|
744
|
+
finding.screenshotPath = `data:image/png;base64,${data.toString("base64")}`;
|
|
745
|
+
} else {
|
|
746
|
+
finding.screenshotPath = null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const html = buildHtml(args, findings, inputPayload.metadata);
|
|
752
|
+
fs.mkdirSync(path.dirname(args.output), { recursive: true });
|
|
753
|
+
fs.writeFileSync(args.output, html, "utf-8");
|
|
754
|
+
|
|
755
|
+
if (findings.length === 0) {
|
|
756
|
+
log.info("Congratulations, no issues found.");
|
|
757
|
+
}
|
|
758
|
+
log.success(`HTML report written to ${args.output}`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
main();
|
|
763
|
+
} catch (error) {
|
|
764
|
+
log.error(`HTML Generation Error: ${error.message}`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|