wcag-a11y 0.4.2 → 0.5.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/README.md +59 -17
- package/dist/cli.js +5 -2
- package/dist/crawler.js +1 -1
- package/dist/demo.js +17 -10
- package/dist/engine/rules/color-contrast.js +72 -14
- package/dist/fixer.js +14 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Dannyplusplus12/WCAG-A11y/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
Most accessibility auditors stop at detection — they tell you *what* is broken and leave the rest to you. `wcag-a11y` crawls your running dev server with Playwright, runs 40+ WCAG 2.1/2.2 checks, and uses AI to generate ready-to-paste fix prompts **or write the fixes directly into your source files**.
|
|
5
|
+
Most accessibility auditors stop at detection — they tell you *what* is broken and leave the rest to you. `wcag-a11y` crawls your running dev server with Playwright, runs 40+ WCAG 2.1/2.2 checks, and uses AI to generate ready-to-paste fix prompts **or write the fixes directly into your source files**. Works with authenticated apps via Playwright session state.
|
|
6
6
|
|
|
7
7
|
Two modes:
|
|
8
8
|
- **`scan`** — find violations + get AI prompts you paste into Cursor, Copilot, or Claude
|
|
@@ -78,13 +78,8 @@ npm install -g wcag-a11y
|
|
|
78
78
|
## Quick start
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
# 1. Configure your AI provider
|
|
82
|
-
wcag-a11y init
|
|
83
|
-
wcag-a11y init --framework react # React / Vite
|
|
84
|
-
wcag-a11y init --framework vue # Vue / Nuxt
|
|
85
|
-
wcag-a11y init --framework angular # Angular
|
|
86
|
-
wcag-a11y init --framework svelte # Svelte / SvelteKit
|
|
87
|
-
wcag-a11y init # plain HTML or auto-detect
|
|
81
|
+
# 1. Configure your AI provider (Gemini is free, no credit card)
|
|
82
|
+
wcag-a11y init
|
|
88
83
|
|
|
89
84
|
# 2. Start your dev server, then scan
|
|
90
85
|
wcag-a11y scan -u http://localhost:3000
|
|
@@ -92,6 +87,12 @@ wcag-a11y scan -u http://localhost:3000
|
|
|
92
87
|
|
|
93
88
|
Add `--pages / /about /contact` to scan specific routes, or `--crawl` to follow links automatically.
|
|
94
89
|
|
|
90
|
+
**Optional — set your framework once:** The tool auto-detects common frameworks at runtime. If detection fails (e.g. scanning a staging URL, or using Astro/SvelteKit), set it in your config so every run uses the right syntax:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
wcag-a11y init --framework next # or react, vue, angular, svelte, astro, …
|
|
94
|
+
```
|
|
95
|
+
|
|
95
96
|
---
|
|
96
97
|
|
|
97
98
|
## Commands
|
|
@@ -120,8 +121,9 @@ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
|
|
|
120
121
|
| `--fast-mode` | off | Output only the raw prompts — no summaries or decoration |
|
|
121
122
|
| `--group <strategy>` | `rule` | `rule`: one prompt per rule type. `none`: one prompt per element |
|
|
122
123
|
| `--ci` | off | Exit with code `1` if any violations are found |
|
|
124
|
+
| `--auth-state <path>` | — | Path to a Playwright `storageState` JSON. Loads cookies and localStorage so you can scan pages behind a login wall |
|
|
123
125
|
| `--provider <name>` | from config | Override AI provider for this run |
|
|
124
|
-
| `--framework <name>` | from config | Override framework for this run
|
|
126
|
+
| `--framework <name>` | from config | *(optional)* Override framework for this run. Auto-detected by default; use this when scanning staging URLs or for frameworks outside the detection list |
|
|
125
127
|
|
|
126
128
|
---
|
|
127
129
|
|
|
@@ -164,34 +166,65 @@ wcag-a11y fix --from-report --apply # patches files from that report, no s
|
|
|
164
166
|
| `-c, --crawl` | off | Auto-discover pages by following same-origin links |
|
|
165
167
|
| `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of rescanning |
|
|
166
168
|
| `--apply` | off | Write fixes to disk (dry-run without this flag) |
|
|
169
|
+
| `--force` | off | Skip the git dirty-state check when using `--apply` |
|
|
167
170
|
| `--provider <name>` | from config | Override AI provider for this run |
|
|
168
|
-
| `--framework <name>` | from config | Override framework for this run
|
|
171
|
+
| `--framework <name>` | from config | *(optional)* Override framework for this run. Auto-detected by default; use this when scanning staging URLs or for frameworks outside the detection list |
|
|
172
|
+
|
|
173
|
+
**Git safety:** `wcag-a11y fix --apply` checks for uncommitted changes before writing anything. If the working tree is dirty it exits with an error — commit or stash first, or pass `--force` to override.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### Scanning authenticated pages
|
|
178
|
+
|
|
179
|
+
Most real apps require a login. Save your session with Playwright once, then reuse it on every scan:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# 1. Save session (run this once after logging in)
|
|
183
|
+
node -e "
|
|
184
|
+
const { chromium } = require('playwright');
|
|
185
|
+
(async () => {
|
|
186
|
+
const browser = await chromium.launch({ headless: false });
|
|
187
|
+
const context = await browser.newContext();
|
|
188
|
+
const page = await context.newPage();
|
|
189
|
+
await page.goto('http://localhost:3000/login');
|
|
190
|
+
// log in manually in the browser window that opens
|
|
191
|
+
await page.waitForTimeout(30000);
|
|
192
|
+
await context.storageState({ path: 'auth.json' });
|
|
193
|
+
await browser.close();
|
|
194
|
+
})();
|
|
195
|
+
"
|
|
196
|
+
|
|
197
|
+
# 2. Scan with your saved session
|
|
198
|
+
wcag-a11y scan -u http://localhost:3000 --auth-state auth.json --pages / /dashboard /settings
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`auth.json` captures cookies and `localStorage`. Keep it out of source control (add `auth.json` to `.gitignore`).
|
|
169
202
|
|
|
170
203
|
---
|
|
171
204
|
|
|
172
205
|
### `wcag-a11y init`
|
|
173
206
|
|
|
174
|
-
Create `a11y.config.json` pre-configured for your chosen provider
|
|
207
|
+
Create `a11y.config.json` pre-configured for your chosen provider.
|
|
175
208
|
|
|
176
209
|
```bash
|
|
177
210
|
wcag-a11y init # Gemini (free, default)
|
|
178
|
-
wcag-a11y init --provider openai
|
|
179
|
-
wcag-a11y init --provider ollama
|
|
180
|
-
|
|
211
|
+
wcag-a11y init --provider openai
|
|
212
|
+
wcag-a11y init --provider ollama # local — no API key needed
|
|
213
|
+
wcag-a11y init --provider openai --framework next # optional: save framework too
|
|
181
214
|
```
|
|
182
215
|
|
|
183
216
|
| Flag | Description |
|
|
184
217
|
|---|---|
|
|
185
218
|
| `--provider <name>` | AI provider. Default: `gemini`. See [AI Providers](#ai-providers) for all options |
|
|
186
|
-
| `--framework <name>` | Your project framework — saved to config so every scan uses it automatically |
|
|
219
|
+
| `--framework <name>` | *(optional)* Your project framework — saved to config so every scan uses it automatically. The tool auto-detects common frameworks; use this flag when scanning staging URLs or using a framework not in the detection list |
|
|
187
220
|
|
|
188
|
-
|
|
221
|
+
Accepted framework values: `next`, `react`, `vue`, `nuxt`, `angular`, `svelte`, `gatsby`, `remix`, `astro` — or any free-form string. You can also add `"framework": "next"` directly to `a11y.config.json` at any time.
|
|
189
222
|
|
|
190
223
|
---
|
|
191
224
|
|
|
192
225
|
### `wcag-a11y demo`
|
|
193
226
|
|
|
194
|
-
Scan a built-in page with
|
|
227
|
+
Scan a built-in page with 11 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
|
|
195
228
|
|
|
196
229
|
```bash
|
|
197
230
|
wcag-a11y demo # violations + AI fix prompts (requires config)
|
|
@@ -227,6 +260,15 @@ All models are configurable. If the AI response is unparseable, the tool generat
|
|
|
227
260
|
|
|
228
261
|
Run `wcag-a11y init` to generate `a11y.config.json`. Only fill in the fields for your chosen provider. This file is gitignored by default.
|
|
229
262
|
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"provider": "gemini",
|
|
266
|
+
"apiKey": "YOUR_GEMINI_API_KEY"
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`"framework"` is optional — add it if auto-detection fails for your setup:
|
|
271
|
+
|
|
230
272
|
```json
|
|
231
273
|
{
|
|
232
274
|
"provider": "gemini",
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ const program = new Command();
|
|
|
13
13
|
program
|
|
14
14
|
.name('wcag-a11y')
|
|
15
15
|
.description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
|
|
16
|
-
.version('0.4.
|
|
16
|
+
.version('0.4.3');
|
|
17
17
|
program
|
|
18
18
|
.command('init')
|
|
19
19
|
.description('Create a11y.config.json in the current directory')
|
|
@@ -35,12 +35,13 @@ program
|
|
|
35
35
|
.option('--fast-mode', 'Output only AI fix prompts — no summaries or explanations', false)
|
|
36
36
|
.option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
|
|
37
37
|
.option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
|
|
38
|
+
.option('--auth-state <path>', 'Path to Playwright storageState JSON for authenticated sessions (e.g. auth.json)')
|
|
38
39
|
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
|
|
39
40
|
.option('--framework <name>', 'Override framework detection for this run (e.g. next, react, vue, angular, svelte, astro)')
|
|
40
41
|
.action(async (opts) => {
|
|
41
42
|
try {
|
|
42
43
|
console.log(`\nScanning ${opts.url}...`);
|
|
43
|
-
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl, framework: opts.framework });
|
|
44
|
+
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl, framework: opts.framework, authState: opts.authState });
|
|
44
45
|
if (opts.terminal && !opts.fastMode) {
|
|
45
46
|
printTerminalReport(result);
|
|
46
47
|
}
|
|
@@ -85,6 +86,7 @@ program
|
|
|
85
86
|
.option('-c, --crawl', 'Auto-discover pages by following same-origin links', false)
|
|
86
87
|
.option('--from-report [path]', 'Use an existing report instead of scanning (default: a11y-report.md)')
|
|
87
88
|
.option('--apply', 'Write fixes to source files (default: dry-run, shows diff only)', false)
|
|
89
|
+
.option('--force', 'Skip git dirty-state check when using --apply', false)
|
|
88
90
|
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
|
|
89
91
|
.option('--framework <name>', 'Override framework detection for this run (e.g. next, react, vue, angular, svelte, astro)')
|
|
90
92
|
.action(async (opts) => {
|
|
@@ -106,6 +108,7 @@ program
|
|
|
106
108
|
crawl: opts.crawl,
|
|
107
109
|
reportPath,
|
|
108
110
|
apply: opts.apply,
|
|
111
|
+
force: opts.force,
|
|
109
112
|
provider,
|
|
110
113
|
srcDir: resolve(process.cwd(), 'src'),
|
|
111
114
|
framework: opts.framework ?? config.framework,
|
package/dist/crawler.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function crawl(options) {
|
|
|
43
43
|
const baseUrl = url.replace(/\/$/, '');
|
|
44
44
|
const browser = await chromium.launch({ headless: true });
|
|
45
45
|
try {
|
|
46
|
-
const context = await browser.newContext();
|
|
46
|
+
const context = await browser.newContext(options.authState ? { storageState: options.authState } : {});
|
|
47
47
|
let pagesToVisit = pages.map((p) => `${baseUrl}${p}`);
|
|
48
48
|
if (autoCrawl) {
|
|
49
49
|
const discoveredPage = await context.newPage();
|
package/dist/demo.js
CHANGED
|
@@ -17,36 +17,43 @@ const DEMO_HTML = `<!DOCTYPE html>
|
|
|
17
17
|
<!-- 1. img-alt: missing alt -->
|
|
18
18
|
<img src="banner.jpg">
|
|
19
19
|
|
|
20
|
-
<!-- 2. color
|
|
20
|
+
<!-- 2. color-contrast: low contrast on solid background (fails 4.5:1) -->
|
|
21
21
|
<p style="color:#aaa;background:#fff;font-size:14px;">
|
|
22
22
|
Summer sale — up to 50% off selected items.
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
-
<!-- 3.
|
|
25
|
+
<!-- 3. color-contrast: rgba(0,0,0,0) transparent bg — walks up to dark parent -->
|
|
26
|
+
<div style="background:#1a1a2e;padding:8px;">
|
|
27
|
+
<p style="color:#888;background:rgba(0,0,0,0);font-size:14px;">
|
|
28
|
+
Free shipping on orders over $50.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- 4. keyboard: div with click but no role/tabindex -->
|
|
26
33
|
<div onclick="addToCart()">Add to Cart</div>
|
|
27
34
|
|
|
28
|
-
<!--
|
|
35
|
+
<!-- 5. form: input with no label -->
|
|
29
36
|
<form id="newsletter">
|
|
30
37
|
<input type="email" placeholder="your@email.com">
|
|
31
38
|
<button type="submit"></button>
|
|
32
39
|
</form>
|
|
33
40
|
|
|
34
|
-
<!--
|
|
41
|
+
<!-- 6. ARIA: invalid role -->
|
|
35
42
|
<div role="widget" id="promo-banner">Special offer!</div>
|
|
36
43
|
|
|
37
|
-
<!--
|
|
44
|
+
<!-- 7. Structure: heading skips h2 → h4 -->
|
|
38
45
|
<h4>Featured Products</h4>
|
|
39
46
|
|
|
40
|
-
<!--
|
|
47
|
+
<!-- 8. Link: non-descriptive text -->
|
|
41
48
|
<a href="/sale">Click here</a>
|
|
42
49
|
|
|
43
|
-
<!--
|
|
50
|
+
<!-- 9. Media: video without captions -->
|
|
44
51
|
<video src="promo.mp4" controls></video>
|
|
45
52
|
|
|
46
|
-
<!--
|
|
53
|
+
<!-- 10. ARIA: aria-hidden but focusable -->
|
|
47
54
|
<button aria-hidden="true" tabindex="0">Hidden action</button>
|
|
48
55
|
|
|
49
|
-
<!--
|
|
56
|
+
<!-- 11. Link: empty anchor -->
|
|
50
57
|
<a href="/about"></a>
|
|
51
58
|
|
|
52
59
|
</body>
|
|
@@ -66,7 +73,7 @@ function startDemoServer() {
|
|
|
66
73
|
export async function runDemo(opts) {
|
|
67
74
|
const { server, url } = await startDemoServer();
|
|
68
75
|
try {
|
|
69
|
-
console.log('\nRunning demo scan against a built-in page with intentional WCAG violations...\n');
|
|
76
|
+
console.log('\nRunning demo scan against a built-in page with 11 intentional WCAG violations...\n');
|
|
70
77
|
const result = await crawl({ url, pages: ['/'] });
|
|
71
78
|
printTerminalReport(result);
|
|
72
79
|
if (opts.ai && result.totalViolations > 0) {
|
|
@@ -26,6 +26,37 @@ export const colorContrastRules = [
|
|
|
26
26
|
}
|
|
27
27
|
return parts.join(' > ') || el.tagName.toLowerCase();
|
|
28
28
|
};
|
|
29
|
+
const parseRgba = (raw) => {
|
|
30
|
+
const m = raw.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
31
|
+
if (!m)
|
|
32
|
+
return null;
|
|
33
|
+
return [+m[1], +m[2], +m[3], m[4] !== undefined ? parseFloat(m[4]) : 1];
|
|
34
|
+
};
|
|
35
|
+
const getOpaqueBg = (el) => {
|
|
36
|
+
let node = el;
|
|
37
|
+
while (node) {
|
|
38
|
+
const rgba = parseRgba(window.getComputedStyle(node).backgroundColor);
|
|
39
|
+
if (rgba && rgba[3] > 0.01)
|
|
40
|
+
return [rgba[0], rgba[1], rgba[2]];
|
|
41
|
+
node = node.parentElement;
|
|
42
|
+
}
|
|
43
|
+
return [255, 255, 255];
|
|
44
|
+
};
|
|
45
|
+
const resolveBackground = (bgRaw, el) => {
|
|
46
|
+
const bgRgba = parseRgba(bgRaw);
|
|
47
|
+
if (!bgRgba || bgRgba[3] <= 0.01)
|
|
48
|
+
return getOpaqueBg(el.parentElement ?? el);
|
|
49
|
+
if (bgRgba[3] < 0.99) {
|
|
50
|
+
const parent = getOpaqueBg(el.parentElement ?? el);
|
|
51
|
+
const a = bgRgba[3];
|
|
52
|
+
return [
|
|
53
|
+
Math.round(bgRgba[0] * a + parent[0] * (1 - a)),
|
|
54
|
+
Math.round(bgRgba[1] * a + parent[1] * (1 - a)),
|
|
55
|
+
Math.round(bgRgba[2] * a + parent[2] * (1 - a)),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
return [bgRgba[0], bgRgba[1], bgRgba[2]];
|
|
59
|
+
};
|
|
29
60
|
const elements = Array.from(document.querySelectorAll('p, span, li, td, th, h1, h2, h3, h4, h5, h6, a, label'));
|
|
30
61
|
const violations = [];
|
|
31
62
|
for (const el of elements) {
|
|
@@ -37,16 +68,14 @@ export const colorContrastRules = [
|
|
|
37
68
|
const isLargeText = fontSize >= 24 || (fontSize >= 18.67 && (fontWeight === 'bold' || parseInt(fontWeight) >= 700));
|
|
38
69
|
if (isLargeText)
|
|
39
70
|
continue;
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const fgMatch = fgRaw.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
43
|
-
const bgMatch = bgRaw.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
44
|
-
if (!fgMatch || !bgMatch)
|
|
71
|
+
const fgRgba = parseRgba(style.color);
|
|
72
|
+
if (!fgRgba)
|
|
45
73
|
continue;
|
|
74
|
+
const [bgR, bgG, bgB] = resolveBackground(style.backgroundColor, el);
|
|
46
75
|
const toLinear = (c) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); };
|
|
47
76
|
const lum = (r, g, b) => 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
48
|
-
const l1 = lum(
|
|
49
|
-
const l2 = lum(
|
|
77
|
+
const l1 = lum(fgRgba[0], fgRgba[1], fgRgba[2]);
|
|
78
|
+
const l2 = lum(bgR, bgG, bgB);
|
|
50
79
|
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
51
80
|
if (ratio < 4.5) {
|
|
52
81
|
violations.push({ selector: getCssPath(el), html: el.outerHTML.slice(0, 200) });
|
|
@@ -82,6 +111,37 @@ export const colorContrastRules = [
|
|
|
82
111
|
}
|
|
83
112
|
return parts.join(' > ') || el.tagName.toLowerCase();
|
|
84
113
|
};
|
|
114
|
+
const parseRgba = (raw) => {
|
|
115
|
+
const m = raw.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
116
|
+
if (!m)
|
|
117
|
+
return null;
|
|
118
|
+
return [+m[1], +m[2], +m[3], m[4] !== undefined ? parseFloat(m[4]) : 1];
|
|
119
|
+
};
|
|
120
|
+
const getOpaqueBg = (el) => {
|
|
121
|
+
let node = el;
|
|
122
|
+
while (node) {
|
|
123
|
+
const rgba = parseRgba(window.getComputedStyle(node).backgroundColor);
|
|
124
|
+
if (rgba && rgba[3] > 0.01)
|
|
125
|
+
return [rgba[0], rgba[1], rgba[2]];
|
|
126
|
+
node = node.parentElement;
|
|
127
|
+
}
|
|
128
|
+
return [255, 255, 255];
|
|
129
|
+
};
|
|
130
|
+
const resolveBackground = (bgRaw, el) => {
|
|
131
|
+
const bgRgba = parseRgba(bgRaw);
|
|
132
|
+
if (!bgRgba || bgRgba[3] <= 0.01)
|
|
133
|
+
return getOpaqueBg(el.parentElement ?? el);
|
|
134
|
+
if (bgRgba[3] < 0.99) {
|
|
135
|
+
const parent = getOpaqueBg(el.parentElement ?? el);
|
|
136
|
+
const a = bgRgba[3];
|
|
137
|
+
return [
|
|
138
|
+
Math.round(bgRgba[0] * a + parent[0] * (1 - a)),
|
|
139
|
+
Math.round(bgRgba[1] * a + parent[1] * (1 - a)),
|
|
140
|
+
Math.round(bgRgba[2] * a + parent[2] * (1 - a)),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
return [bgRgba[0], bgRgba[1], bgRgba[2]];
|
|
144
|
+
};
|
|
85
145
|
const elements = Array.from(document.querySelectorAll('p, span, h1, h2, h3, h4, h5, h6'));
|
|
86
146
|
const violations = [];
|
|
87
147
|
for (const el of elements) {
|
|
@@ -93,16 +153,14 @@ export const colorContrastRules = [
|
|
|
93
153
|
const isLargeText = fontSize >= 24 || (fontSize >= 18.67 && (fontWeight === 'bold' || parseInt(fontWeight) >= 700));
|
|
94
154
|
if (!isLargeText)
|
|
95
155
|
continue;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const fgMatch = fgRaw.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
99
|
-
const bgMatch = bgRaw.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
100
|
-
if (!fgMatch || !bgMatch)
|
|
156
|
+
const fgRgba = parseRgba(style.color);
|
|
157
|
+
if (!fgRgba)
|
|
101
158
|
continue;
|
|
159
|
+
const [bgR, bgG, bgB] = resolveBackground(style.backgroundColor, el);
|
|
102
160
|
const toLinear = (c) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); };
|
|
103
161
|
const lum = (r, g, b) => 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
104
|
-
const l1 = lum(
|
|
105
|
-
const l2 = lum(
|
|
162
|
+
const l1 = lum(fgRgba[0], fgRgba[1], fgRgba[2]);
|
|
163
|
+
const l2 = lum(bgR, bgG, bgB);
|
|
106
164
|
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
107
165
|
if (ratio < 3) {
|
|
108
166
|
violations.push({ selector: getCssPath(el), html: el.outerHTML.slice(0, 200) });
|
package/dist/fixer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
2
3
|
import { join, resolve } from 'path';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import { crawl } from './crawler.js';
|
|
@@ -54,6 +55,19 @@ export async function runFix(opts) {
|
|
|
54
55
|
console.log(`Grouped ${locatedCount} violation(s) across ${fileGroups.size} file(s).\n`);
|
|
55
56
|
if (!opts.apply)
|
|
56
57
|
console.log(chalk.gray('Dry-run mode — use --apply to write changes.\n'));
|
|
58
|
+
if (opts.apply && !opts.force) {
|
|
59
|
+
try {
|
|
60
|
+
const dirty = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
61
|
+
if (dirty) {
|
|
62
|
+
console.error(chalk.red('Error: uncommitted changes detected. Commit or stash them first, or run with --force to override.\n'));
|
|
63
|
+
console.error(chalk.gray(dirty.split('\n').slice(0, 5).join('\n')));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// not a git repo or git unavailable — proceed without guard
|
|
69
|
+
}
|
|
70
|
+
}
|
|
57
71
|
let modifiedFiles = 0;
|
|
58
72
|
let modifiedViolations = 0;
|
|
59
73
|
for (const [filePath, violations] of fileGroups) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wcag-a11y",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes. Crawls your dev server with Playwright, runs 40+ checks, and uses AI (12 providers) to generate fix prompts or patch source files directly.",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes. Crawls your dev server with Playwright, runs 40+ checks, and uses AI (12 providers) to generate fix prompts or patch source files directly. Supports authenticated sessions via Playwright storageState.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wcag",
|
|
7
7
|
"accessibility",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"gemini",
|
|
19
19
|
"openai",
|
|
20
20
|
"anthropic",
|
|
21
|
+
"auth",
|
|
21
22
|
"screen-reader"
|
|
22
23
|
],
|
|
23
24
|
"type": "module",
|