inspectra-engine 0.0.0-stage → 1.0.2
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/CHANGELOG.md +58 -0
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +132 -2
- package/bin/inspectra.mjs +68 -0
- package/client/index.d.mts +85 -0
- package/client/index.mjs +2 -0
- package/compare.js +439 -0
- package/dom-compare.js +263 -0
- package/examples/README.md +105 -0
- package/examples/figma-scan.json +12 -0
- package/examples/png-metadata.json +5 -0
- package/examples/runner-scan.json +7 -0
- package/node/analysis-worker.mjs +33 -0
- package/node/dev-config.mjs +18 -0
- package/node/event-stream.mjs +10 -0
- package/node/index.d.mts +114 -0
- package/node/index.mjs +100 -0
- package/node/job-manager.mjs +172 -0
- package/node/png-highlights.mjs +30 -0
- package/node/runner.mjs +135 -0
- package/package.json +136 -4
- package/pages.json +8 -0
- package/scan.js +27 -0
- package/scanner/README.md +70 -0
- package/scanner/accessibility.mjs +10 -0
- package/scanner/browser-snapshot.js +128 -0
- package/scanner/figma-snapshot.mjs +221 -0
- package/scanner/legacy-image-tools.cjs +14 -0
- package/scanner/ocr/local.mjs +52 -0
- package/scanner/ocr/vision.swift +29 -0
- package/scanner/png-regions.mjs +53 -0
- package/scanner/png-snapshot.mjs +192 -0
- package/scripts/check-build.mjs +12 -0
- package/server.js +361 -0
- package/ui/compare-2.html +946 -0
- package/ui/compare-3.html +1222 -0
- package/ui/compare.html +1408 -0
- package/ui/index.html +136 -0
- package/ui-check.js +1493 -0
package/ui/index.html
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Accessibility Scanner</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{--bg:#f7f8fa;--card:#fff;--fg:#14161a;--mut:#667085;--line:#e6e8ec;--soft:#f2f4f7;--acc:#5b5bf0;--acc2:#8b5cf6;--good:#0a7a3d;--avg:#c33300;--bad:#c7221f;--r:14px}
|
|
9
|
+
@media(prefers-color-scheme:dark){:root{--bg:#0d0f13;--card:#161920;--fg:#eceef2;--mut:#98a2b3;--line:#262a33;--soft:#1d212a;--good:#3ddc84;--avg:#ffb020;--bad:#ff6b61}}
|
|
10
|
+
*{box-sizing:border-box}
|
|
11
|
+
body{font:15px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;max-width:1180px;margin:0 auto;padding:2.2rem 1.2rem 4rem;color:var(--fg);background:var(--bg);-webkit-font-smoothing:antialiased}
|
|
12
|
+
h1{font-size:1.9rem;letter-spacing:-.03em;margin:0 0 1.2rem;background:linear-gradient(90deg,var(--acc),var(--acc2));-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
13
|
+
h2{font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin:2rem 0 .6rem;font-weight:600}
|
|
14
|
+
a{color:var(--acc)}
|
|
15
|
+
code{font:.85em ui-monospace,SFMono-Regular,Menlo,monospace;background:var(--soft);padding:.1em .35em;border-radius:5px}
|
|
16
|
+
form{display:flex;gap:.6rem;flex-wrap:wrap;background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:1rem;box-shadow:0 1px 2px rgba(16,24,40,.04)}
|
|
17
|
+
label.opt:first-child{color:var(--mut);font-size:.8rem;font-weight:500}
|
|
18
|
+
input[type=url]{flex:1;min-width:240px;padding:.7rem .9rem;font:inherit;color:inherit;background:var(--bg);border:1px solid var(--line);border-radius:10px;outline:0;transition:.15s}
|
|
19
|
+
input[type=url]:focus{border-color:var(--acc);box-shadow:0 0 0 3px color-mix(in srgb,var(--acc) 22%,transparent)}
|
|
20
|
+
button{padding:.7rem 1.4rem;font:600 .95rem ui-sans-serif,system-ui,sans-serif;border:0;border-radius:10px;background:linear-gradient(135deg,var(--acc),var(--acc2));color:#fff;cursor:pointer;transition:.15s}
|
|
21
|
+
button:hover{filter:brightness(1.08)}button:active{transform:scale(.98)}
|
|
22
|
+
button:disabled{opacity:.55;cursor:progress}
|
|
23
|
+
.opt{width:100%;font-size:.85rem;color:var(--mut)}
|
|
24
|
+
input[type=checkbox]{accent-color:var(--acc);margin-right:.4rem}
|
|
25
|
+
#out>p:first-child{display:inline-block;background:var(--soft);border-radius:99px;padding:.25rem .8rem;font-size:.85rem;color:var(--mut);margin:1.2rem 0 0}
|
|
26
|
+
.sum{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:.7rem;margin:1rem 0}
|
|
27
|
+
.sum div{padding:.8rem 1rem;border-radius:var(--r);background:var(--card);border:1px solid var(--line);color:var(--mut);font-size:.85rem}
|
|
28
|
+
.sum b{display:block;font-size:1.5rem;color:var(--fg);letter-spacing:-.02em}
|
|
29
|
+
details{border:1px solid var(--line);background:var(--card);border-radius:12px;margin:.5rem 0;padding:.7rem 1rem}
|
|
30
|
+
details[open]{box-shadow:0 1px 3px rgba(16,24,40,.06)}
|
|
31
|
+
summary{cursor:pointer;font-weight:600;list-style:none;display:flex;align-items:center;gap:.3rem}
|
|
32
|
+
summary::-webkit-details-marker{display:none}
|
|
33
|
+
summary::before{content:"›";color:var(--mut);display:inline-block;width:1rem;transition:transform .15s}
|
|
34
|
+
details[open]>summary::before{transform:rotate(90deg)}
|
|
35
|
+
summary:focus-visible,button:focus-visible{outline:2px solid var(--acc);outline-offset:2px;border-radius:6px}
|
|
36
|
+
.tag{font-size:.7rem;font-weight:600;text-transform:capitalize;padding:.1rem .55rem;border-radius:99px;color:#fff;margin-right:.4rem}
|
|
37
|
+
.critical{background:#d92d20}.serious{background:#e5590a}.moderate{background:#a16207}.minor{background:#667085}
|
|
38
|
+
pre{background:var(--soft);padding:.6rem .8rem;border-radius:8px;overflow:auto;white-space:pre-wrap;word-break:break-all;font:.8rem ui-monospace,Menlo,monospace}
|
|
39
|
+
.gauges{display:flex;gap:1.2rem;justify-content:center;flex-wrap:wrap;margin:.6rem 0;background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:1.4rem 1rem}
|
|
40
|
+
.gauge{text-align:center;width:104px}
|
|
41
|
+
.gauge svg{width:84px;height:84px;transform:rotate(-90deg)}
|
|
42
|
+
.gauge .bg{fill:none;stroke-width:7;opacity:.15}
|
|
43
|
+
.gauge .fg{fill:none;stroke-width:7;stroke-linecap:round;transition:stroke-dashoffset .8s}
|
|
44
|
+
.gauge .n{font:600 1.5rem/84px ui-sans-serif,system-ui;position:absolute;inset:0 0 auto 0;height:84px;letter-spacing:-.02em}
|
|
45
|
+
.gauge .wrap{position:relative;width:84px;margin:0 auto}
|
|
46
|
+
.gauge .l{margin-top:.5rem;font-size:.82rem;font-weight:500}
|
|
47
|
+
.gauge small{display:block;color:var(--mut);font-size:.7rem}
|
|
48
|
+
.g-good{color:var(--good);stroke:#0cce6b}.g-avg{color:var(--avg);stroke:#ffa400}.g-bad{color:var(--bad);stroke:#ff4e42}
|
|
49
|
+
.g-na{color:var(--mut);stroke:#999}
|
|
50
|
+
.pill{display:inline-block;height:auto;font:600 1rem ui-sans-serif,system-ui;padding:.4rem .9rem;border-radius:99px;margin-top:1.4rem;background:color-mix(in srgb,var(--good) 14%,transparent);stroke:none}
|
|
51
|
+
.pill.g-avg{background:color-mix(in srgb,var(--avg) 14%,transparent)}.pill.g-bad{background:color-mix(in srgb,var(--bad) 14%,transparent)}
|
|
52
|
+
.gauge .wrap:has(.pill){height:84px}
|
|
53
|
+
.lh-cat{margin-top:.7rem}
|
|
54
|
+
.lh-cat>summary{font-size:1rem}
|
|
55
|
+
.lh-cat h3{font-size:.8rem;color:var(--mut);text-transform:uppercase;letter-spacing:.05em;margin:1.1rem 0 .3rem}
|
|
56
|
+
.lh-a{border:0;border-top:1px solid var(--line);border-radius:0;margin:0;padding:.5rem 0;box-shadow:none!important;background:none}
|
|
57
|
+
.lh-a>summary{font-weight:500;font-size:.92rem}
|
|
58
|
+
.dot{display:inline-block;flex:none;width:.55rem;height:.55rem;border-radius:50%;margin-right:.3rem}
|
|
59
|
+
.dot.fail,.dot.error{background:#ff4e42}.dot.pass{background:#0cce6b}.dot.manual,.dot.info,.dot.na{background:#98a2b3}
|
|
60
|
+
.val{color:var(--bad);font-size:.8rem;margin-left:.4rem;font-weight:500}
|
|
61
|
+
.lh-a p{margin:.3rem 0 .3rem 1.5rem;font-size:.88rem;color:var(--mut)}
|
|
62
|
+
.tw{overflow:auto;margin:.3rem 0 .3rem 1.5rem;border:1px solid var(--line);border-radius:10px}
|
|
63
|
+
table{border-collapse:collapse;font-size:.78rem;width:100%}
|
|
64
|
+
th,td{border-bottom:1px solid var(--line);padding:.35rem .6rem;text-align:left;vertical-align:top;word-break:break-all;max-width:420px}
|
|
65
|
+
tr:last-child td{border-bottom:0}
|
|
66
|
+
th{background:var(--soft);font-weight:600;color:var(--mut)}
|
|
67
|
+
.full{display:inline-block;margin:.6rem 0;font-size:.88rem;font-weight:500;text-decoration:none}
|
|
68
|
+
.full:hover{text-decoration:underline}
|
|
69
|
+
.err{color:var(--bad)}
|
|
70
|
+
.nav{display:flex;gap:.6rem;margin-bottom:1.8rem;border-bottom:1px solid var(--line);padding-bottom:.8rem}
|
|
71
|
+
.nav-link{text-decoration:none;font-weight:600;font-size:.9rem;padding:.4rem .9rem;border-radius:8px;color:var(--mut);transition:.15s}
|
|
72
|
+
.nav-link:hover{color:var(--fg);background:var(--soft)}
|
|
73
|
+
.nav-link.active{color:#fff;background:linear-gradient(135deg,var(--acc),var(--acc2))}
|
|
74
|
+
@media(max-width:520px){body{padding-top:1.5rem}.gauge{width:90px}}
|
|
75
|
+
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<nav class="nav">
|
|
80
|
+
<a href="/" class="nav-link active">Accessibility Scanner</a>
|
|
81
|
+
<a href="/compare" class="nav-link">Visual Regression Comparison</a>
|
|
82
|
+
<a href="/compare-2" class="nav-link">UI Differences</a>
|
|
83
|
+
<a href="/compare-3" class="nav-link">UI Completeness Check</a>
|
|
84
|
+
</nav>
|
|
85
|
+
<h1>Accessibility Scanner</h1>
|
|
86
|
+
<form id="f">
|
|
87
|
+
<label for="u" class="opt">Page URL</label>
|
|
88
|
+
<input id="u" type="url" required placeholder="https://your-site.com" autofocus>
|
|
89
|
+
<button id="b">Scan</button>
|
|
90
|
+
<label class="opt"><input type="checkbox" id="bp" checked> Include best-practice rules (beyond WCAG A/AA)</label>
|
|
91
|
+
</form>
|
|
92
|
+
<div id="out" aria-live="polite"></div>
|
|
93
|
+
<script>
|
|
94
|
+
const gauge=(label,s)=>{
|
|
95
|
+
const v=s.score,cls=v==null?'g-na':v>=90?'g-good':v>=50?'g-avg':'g-bad',C=2*Math.PI*45;
|
|
96
|
+
return `<div class="gauge ${cls}"><div class="wrap"><svg viewBox="0 0 100 100" aria-hidden="true"><circle class="bg" cx="50" cy="50" r="45" stroke="currentColor"/>
|
|
97
|
+
<circle class="fg" cx="50" cy="50" r="45" stroke-dasharray="${C}" stroke-dashoffset="${C*(1-(v||0)/100)}"/></svg>
|
|
98
|
+
<span class="n">${v==null?'–':v}</span></div><div class="l">${label}</div>${s.failed==null?'':`<small>${s.failed} failed · ${s.passed} passed</small>`}</div>`;
|
|
99
|
+
};
|
|
100
|
+
const agentic=a=>a?`<div class="gauge"><div class="wrap"><span class="pill ${a.passed===a.total?'g-good':a.passed?'g-avg':'g-bad'}">${a.passed}/${a.total}</span></div><div class="l">Agentic Browsing</div></div>`:'';
|
|
101
|
+
const md=s=>esc(s).replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>').replace(/`([^`]+)`/g,'<code>$1</code>');
|
|
102
|
+
const lhAudit=a=>`<details class="lh-a"><summary><span class="dot ${a.status}"></span>${esc(a.title)}${a.displayValue?`<span class="val">${esc(a.displayValue)}</span>`:''}</summary>
|
|
103
|
+
<p>${md(a.description||'')}</p>${a.details?`<div class="tw"><table><thead><tr>${a.details.headings.map(x=>`<th>${esc(x)}</th>`).join('')}</tr></thead><tbody>${a.details.rows.map(r=>`<tr>${r.map(c=>`<td>${esc(c)}</td>`).join('')}</tr>`).join('')}</tbody></table>${a.details.total>a.details.rows.length?`<small>Showing ${a.details.rows.length} of ${a.details.total}</small>`:''}</div>`:''}</details>`;
|
|
104
|
+
const lhSection=(t,list,open)=>list.length?`<h3>${t} (${list.length})</h3>${open?list.map(lhAudit).join(''):`<details><summary>Show</summary>${list.map(lhAudit).join('')}</details>`}`:'';
|
|
105
|
+
const lhCategory=c=>{
|
|
106
|
+
const by=s=>c.audits.filter(a=>s.includes(a.status));
|
|
107
|
+
const bad=by(['fail','error']).sort((x,y)=>(x.score??0)-(y.score??0));
|
|
108
|
+
return `<details class="lh-cat" ${bad.length?'open':''}><summary>${esc(c.title)} — ${bad.length} to improve</summary>
|
|
109
|
+
${bad.length?lhSection('Needs improvement',bad,true):'<p>Nothing to improve.</p>'}
|
|
110
|
+
${lhSection('Manual checks',by(['manual']),false)}${lhSection('Additional info',by(['info']),false)}
|
|
111
|
+
${lhSection('Passed audits',by(['pass']),false)}${lhSection('Not applicable',by(['na']),false)}</details>`;
|
|
112
|
+
};
|
|
113
|
+
const esc=s=>String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
114
|
+
f.onsubmit=async e=>{
|
|
115
|
+
e.preventDefault(); b.disabled=true; out.textContent='Scanning… this can take 10-30 seconds.';
|
|
116
|
+
try{
|
|
117
|
+
const r=await fetch('/scan',{method:'POST',body:JSON.stringify({url:u.value.trim(),bestPractice:bp.checked})});
|
|
118
|
+
const d=await r.json(); if(d.error) throw new Error(d.error);
|
|
119
|
+
out.innerHTML=`<p>Scanned <strong>${esc(d.url)}</strong></p>
|
|
120
|
+
<h2>Lighthouse</h2>
|
|
121
|
+
${d.lighthouse?`<div class="gauges" role="group" aria-label="Lighthouse scores">${[['performance','Performance'],['accessibility','Accessibility'],['best-practices','Best Practices'],['seo','SEO']].map(([k,l])=>gauge(l,{score:d.lighthouse[k]})).join('')}${agentic(d.lighthouse['agentic-browsing'])}</div>`:`<p class="err">Lighthouse scores unavailable: ${esc(d.lighthouseError||'unknown error')}</p>`}
|
|
122
|
+
${d.reportId?`<a class="full" href="/report/${d.reportId}" target="_blank" rel="noopener">Open full Lighthouse report (same as Chrome) ↗</a>`:''}
|
|
123
|
+
${(d.lighthouseCategories||[]).map(lhCategory).join('')}
|
|
124
|
+
<h2>WCAG (axe-core)</h2>
|
|
125
|
+
<div class="gauges" role="group" aria-label="WCAG scores">${gauge('WCAG overall',d.scores.overall)}${gauge('WCAG A',d.scores.a)}${gauge('WCAG AA',d.scores.aa)}</div>
|
|
126
|
+
<div class="sum"><div><b>${d.counts.violations}</b> violations</div><div><b>${d.counts.incomplete}</b> need review</div><div><b>${d.counts.passes}</b> passed</div></div>`+
|
|
127
|
+
(d.violations.length?'':'<p>No WCAG A/AA violations found automatically. Manual checks are still needed.</p>')+
|
|
128
|
+
d.violations.map(v=>`<details><summary><span class="tag ${v.impact}">${v.impact}</span>${esc(v.help)} <small>(${v.totalNodes} elements)</small></summary>
|
|
129
|
+
<p>Rule <code>${esc(v.id)}</code> · ${esc(v.tags.join(', '))} · <a href="${esc(v.helpUrl)}" target="_blank" rel="noopener">How to fix</a></p>
|
|
130
|
+
${v.nodes.map(n=>`<p><code>${esc(n.target)}</code></p><pre>${esc(n.html)}</pre><pre>${esc(n.fix)}</pre>`).join('')}</details>`).join('');
|
|
131
|
+
}catch(err){out.innerHTML=`<p class="err">Error: ${esc(err.message)}</p>`}
|
|
132
|
+
b.disabled=false;
|
|
133
|
+
};
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|