wcag-a11y 0.4.0 → 0.4.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/README.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  [![CI](https://github.com/Dannyplusplus12/WCAG-A11y/actions/workflows/ci.yml/badge.svg)](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` goes further. It 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**.
6
6
 
7
7
  Two modes:
8
- - **`scan`** — find violations + generate AI prompts you paste into Cursor, Copilot, or Claude
8
+ - **`scan`** — find violations + get AI prompts you paste into Cursor, Copilot, or Claude
9
9
  - **`fix`** — find violations + patch source files automatically (dry-run by default, `--apply` to write)
10
10
 
11
11
  ---
12
12
 
13
13
  ## Try it instantly
14
14
 
15
- No dev server, no config, no setup:
15
+ No dev server, no config:
16
16
 
17
17
  ```bash
18
18
  npx wcag-a11y demo
@@ -22,41 +22,50 @@ npx wcag-a11y demo
22
22
 
23
23
  ## What the output looks like
24
24
 
25
- Running a scan prints a violation summary per page, then AI-generated fixes for each rule:
26
-
27
25
  ```
28
- Scanning http://localhost:3000...
29
-
30
- /
31
- ✖ critical img-alt 3 violations
32
- ✖ serious color-contrast-text 2 violations
33
- ✖ serious label-missing 1 violation
34
- ✖ moderate no-positive-tabindex 1 violation
35
-
36
- 7 violations across 1 page
37
-
38
- Generating AI fixes for 7 violations...
39
-
40
- ────────────────────────────────────────────
41
- [img-alt] 3 elements affected
42
- #hero-img #logo #banner
43
-
44
- Why it matters:
45
- Screen readers cannot describe the image to blind users without an alt attribute.
46
- Users relying on assistive technology receive no information about the image content.
47
-
48
- Fixed HTML:
49
- <img src="banner.jpg" alt="Summer sale — up to 50% off">
50
-
51
- Prompt for your AI editor:
52
- Fix accessibility: 3 <img> elements (#hero-img, #logo, #banner) are missing alt
53
- attributes, violating WCAG 1.1.1. Add descriptive alt text to each image.
54
- ────────────────────────────────────────────
26
+ WCAG A11y — scan complete
27
+ ────────────────────────────────────────────────────────────
28
+
29
+ http://localhost:3000 3 critical 4 serious 3 moderate
30
+
31
+ [CRITICAL] Images must have an alt attribute WCAG 1.1.1
32
+ img
33
+ [CRITICAL] Form inputs must have an associated label WCAG 1.3.1
34
+ input[type="email"]
35
+ [CRITICAL] Buttons must have an accessible name WCAG 4.1.2
36
+ button[type="submit"]
37
+ [SERIOUS] Normal text must meet 4.5:1 contrast ratio WCAG 1.4.3
38
+ → p
39
+ [SERIOUS] Links must have descriptive text WCAG 2.4.4
40
+ → a[href="/sale"]
41
+ … and 5 more
42
+
43
+ ────────────────────────────────────────────────────────────
44
+ Total: 3 critical · 4 serious · 3 moderate
45
+
46
+ AI Fix Prompts — paste any of these into Cursor, Copilot, or Claude
47
+ ────────────────────────────────────────────────────────────
48
+
49
+ [img-alt]
50
+ img
51
+ Screen reader users hear nothing for this image branding, instructions,
52
+ or data it conveys is completely invisible to them.
53
+ ┌─ Copy this prompt ──────────────────────────────────────
54
+ │ Fix WCAG 1.1.1 (Level A) — img is missing an alt attribute
55
+
56
+ │ Current HTML:
57
+ │ <img src="banner.jpg">
58
+
59
+ │ How to fix:
60
+ │ Add alt text describing the image content.
61
+ │ Use alt="" if the image is purely decorative.
62
+ │ Example: <img src="banner.jpg" alt="Summer sale — 50% off">
63
+ └─────────────────────────────────────────────────────────
64
+
65
+ … 9 more prompts — full report saved to a11y-report.md
55
66
  ```
56
67
 
57
- The **prompt** at the end of each fix is what you copy into Cursor, Copilot, or Claude. It includes the affected selectors, the WCAG rule, and exactly what needs to change no rewriting needed.
58
-
59
- The full output is saved to `a11y-report.md` by default. Pass `--no-report` to skip.
68
+ Each prompt tells you the WCAG criterion, shows the broken element, and gives the exact fix ready to paste into your AI editor.
60
69
 
61
70
  ---
62
71
 
@@ -66,62 +75,27 @@ The full output is saved to `a11y-report.md` by default. Pass `--no-report` to s
66
75
  npm install -g wcag-a11y
67
76
  ```
68
77
 
69
- ## Setup
78
+ ## Quick start
70
79
 
71
80
  ```bash
72
- wcag-a11y init # Gemini (free, default)
73
- wcag-a11y init --provider openai # OpenAI
74
- wcag-a11y init --provider anthropic # Anthropic Claude
75
- wcag-a11y init --provider mistral # Mistral
76
- wcag-a11y init --provider groq # Groq (fast inference)
77
- wcag-a11y init --provider cohere # Cohere
78
- wcag-a11y init --provider xai # xAI Grok
79
- wcag-a11y init --provider deepseek # DeepSeek
80
- wcag-a11y init --provider together # Together AI (open-source models)
81
- wcag-a11y init --provider perplexity # Perplexity
82
- wcag-a11y init --provider azure-openai # Azure OpenAI
83
- wcag-a11y init --provider ollama # Local — no API key needed
81
+ # 1. Configure your AI provider and framework (Gemini is free, no credit card)
82
+ wcag-a11y init --framework next # Next.js
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
88
+
89
+ # 2. Start your dev server, then scan
90
+ wcag-a11y scan -u http://localhost:3000
84
91
  ```
85
92
 
86
- Each command creates an `a11y.config.json` pre-wired for that provider. Fill in your API key, then scan.
93
+ Add `--pages / /about /contact` to scan specific routes, or `--crawl` to follow links automatically.
87
94
 
88
95
  ---
89
96
 
90
97
  ## Commands
91
98
 
92
- ### `wcag-a11y demo`
93
-
94
- Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
95
-
96
- ```bash
97
- wcag-a11y demo # violations + AI fixes (default, requires config)
98
- wcag-a11y demo --no-ai # violations only, no AI — faster
99
- wcag-a11y demo --no-report # skip saving a11y-report.md
100
- ```
101
-
102
- | Flag | Description |
103
- |---|---|
104
- | `--no-ai` | Skip AI fix generation — prints violations only, no prompts |
105
- | `--no-report` | Skip saving report to `a11y-report.md` (report is saved by default) |
106
-
107
- ---
108
-
109
- ### `wcag-a11y init`
110
-
111
- Create `a11y.config.json` in the current directory, pre-configured for your chosen provider.
112
-
113
- ```bash
114
- wcag-a11y init # Gemini (default)
115
- wcag-a11y init --provider openai # OpenAI
116
- wcag-a11y init --provider ollama # Ollama (local)
117
- ```
118
-
119
- | Flag | Description |
120
- |---|---|
121
- | `--provider <name>` | Which provider to configure. Valid values: `gemini` (default), `openai`, `anthropic`, `mistral`, `groq`, `cohere`, `xai`, `deepseek`, `together`, `perplexity`, `azure-openai`, `ollama`. Determines which fields are written to the config file. |
122
-
123
- ---
124
-
125
99
  ### `wcag-a11y scan`
126
100
 
127
101
  Scan a running dev server for accessibility violations.
@@ -137,16 +111,17 @@ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
137
111
  | Flag | Default | Description |
138
112
  |---|---|---|
139
113
  | `-u, --url <url>` | required | Base URL of your running dev server |
140
- | `-p, --pages <pages...>` | `/` | One or more paths to scan. Separate with spaces: `--pages / /about /contact` |
141
- | `-c, --crawl` | off | Follow same-origin links and scan all reachable pages automatically |
142
- | `--no-report` | on | Skip saving scan output to `a11y-report.md` (report is saved by default) |
143
- | `--no-ai` | on | Skip AI fix generation — scan runs faster and prints violations only |
144
- | `--no-explain` | on | Print only the ready-to-paste prompt for each fix, without the AI explanation |
145
- | `--terminal` | off | Print violations summary and AI fix prompts to terminal |
146
- | `--fast-mode` | off | Output only AI fix prompts — no summaries, explanations, or progress messages |
147
- | `--group <strategy>` | `rule` | `rule` (default) groups all violations of the same type into one fix prompt. `none` produces a separate prompt per element. Use `none` when violations of the same rule need different fixes |
148
- | `--ci` | off | Exit with code `1` if any violations are found. Use this to fail a CI pipeline |
149
- | `--provider <name>` | from config | Override the AI provider for this run. See [AI Providers](#ai-providers) for valid names. Does not modify the config file |
114
+ | `-p, --pages <paths...>` | `/` | Paths to scan. Space-separated: `--pages / /about /contact` |
115
+ | `-c, --crawl` | off | Follow same-origin links and scan all reachable pages |
116
+ | `--no-ai` | | Skip AI fix generation scan runs faster, violations only |
117
+ | `--no-report` | | Skip saving `a11y-report.md` |
118
+ | `--no-explain` | | Omit explanations, show prompts only |
119
+ | `--terminal` | off | Print violations and AI prompts to terminal |
120
+ | `--fast-mode` | off | Output only the raw prompts — no summaries or decoration |
121
+ | `--group <strategy>` | `rule` | `rule`: one prompt per rule type. `none`: one prompt per element |
122
+ | `--ci` | off | Exit with code `1` if any violations are found |
123
+ | `--provider <name>` | from config | Override AI provider for this run |
124
+ | `--framework <name>` | from config | Override framework for this run (e.g. `next`, `react`, `vue`, `angular`, `svelte`, `astro`) |
150
125
 
151
126
  ---
152
127
 
@@ -155,34 +130,13 @@ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
155
130
  Scan for violations and apply AI-generated patches directly to your source files. Works with any framework — React, Vue, Angular, Svelte, or plain HTML.
156
131
 
157
132
  ```bash
158
- # Dry-run: scan and show what would change (safe, no files written)
159
- wcag-a11y fix -u http://localhost:3000
160
-
161
- # Preview specific pages
162
- wcag-a11y fix -u http://localhost:3000 --pages / /about /contact
163
-
164
- # Write fixes to disk
165
- wcag-a11y fix -u http://localhost:3000 --apply
166
-
167
- # Auto-discover pages + write fixes
168
- wcag-a11y fix -u http://localhost:3000 --crawl --apply
169
-
170
- # Skip rescanning — load violations from an existing report
171
- wcag-a11y fix --from-report
172
- wcag-a11y fix --from-report ./reports/a11y-report.md --apply
133
+ wcag-a11y fix -u http://localhost:3000 # dry-run: show diff, nothing written
134
+ wcag-a11y fix -u http://localhost:3000 --apply # write fixes to disk
135
+ wcag-a11y fix --from-report --apply # patch from an existing report
173
136
  ```
174
137
 
175
- **How it works:**
176
-
177
- 1. Runs the same scan as `wcag-a11y scan` (or loads an existing report with `--from-report`)
178
- 2. For each violation, locates the source file — checks `violation.source` (React dev mode) first, then falls back to grepping `./src` for unique identifiers in the HTML snippet (`id=`, `name=`, `for=`, local `src=`, text content)
179
- 3. Groups violations by file (multiple violations in the same file → one AI call)
180
- 4. Sends the full file content + violation list to your configured AI provider and asks for the corrected file
181
- 5. Shows a colored diff before writing anything
182
- 6. With `--apply`, overwrites the file; without it, only prints the diff
183
-
184
138
  ```
185
- src/components/Navbar.jsx — 2 violation(s)
139
+ src/components/Navbar.jsx — 2 violations
186
140
  · [button-name] Buttons must have an accessible name
187
141
  · [aria-valid-role] Elements must use valid ARIA roles
188
142
 
@@ -196,52 +150,93 @@ src/components/Navbar.jsx — 2 violation(s)
196
150
  + <li>Home</li>
197
151
  ```
198
152
 
153
+ **Common workflow:** scan first to review, then patch:
154
+
155
+ ```bash
156
+ wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md
157
+ wcag-a11y fix --from-report --apply # patches files from that report, no second crawl
158
+ ```
159
+
199
160
  | Flag | Default | Description |
200
161
  |---|---|---|
201
- | `-u, --url <url>` | — | Base URL of your running dev server. Required unless `--from-report` is used |
202
- | `-p, --pages <pages...>` | `/` | Specific pages to scan |
162
+ | `-u, --url <url>` | — | Base URL. Required unless `--from-report` is used |
163
+ | `-p, --pages <paths...>` | `/` | Paths to scan |
203
164
  | `-c, --crawl` | off | Auto-discover pages by following same-origin links |
204
- | `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of scanning. Useful when you already ran `scan --report` and just want to apply fixes |
205
- | `--apply` | off | Write patched files to disk (dry-run without this flag) |
206
- | `--provider <name>` | from config | Override AI provider for this run. See [AI Providers](#ai-providers) for valid names |
165
+ | `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of rescanning |
166
+ | `--apply` | off | Write fixes to disk (dry-run without this flag) |
167
+ | `--provider <name>` | from config | Override AI provider for this run |
168
+ | `--framework <name>` | from config | Override framework for this run (e.g. `next`, `react`, `vue`, `angular`, `svelte`, `astro`) |
169
+
170
+ ---
207
171
 
208
- > **Tip:** Always run without `--apply` first to review the diff. The dry-run is safe — nothing is written to disk.
172
+ ### `wcag-a11y init`
209
173
 
210
- **Common workflow:** run `scan --report` to generate a report for review, then run `fix --from-report --apply` to patch the files — no second browser crawl needed.
174
+ Create `a11y.config.json` pre-configured for your chosen provider and framework.
211
175
 
212
176
  ```bash
213
- wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md automatically
214
- wcag-a11y fix --from-report --apply # patch files from that report
177
+ wcag-a11y init # Gemini (free, default)
178
+ wcag-a11y init --provider openai --framework next # OpenAI + Next.js
179
+ wcag-a11y init --provider ollama --framework react # local Ollama + React
180
+ # … 12 providers total, any framework string accepted
181
+ ```
182
+
183
+ | Flag | Description |
184
+ |---|---|
185
+ | `--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 |
187
+
188
+ Framework is saved as `"framework"` in `a11y.config.json`. You can also edit the file directly at any time. Supported values for best results: `next`, `react`, `vue`, `nuxt`, `angular`, `svelte`, `gatsby`, `remix`, `astro` — or any free-form string.
189
+
190
+ ---
191
+
192
+ ### `wcag-a11y demo`
193
+
194
+ Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
195
+
196
+ ```bash
197
+ wcag-a11y demo # violations + AI fix prompts (requires config)
198
+ wcag-a11y demo --no-ai # violations only, no AI
215
199
  ```
216
200
 
217
201
  ---
218
202
 
219
203
  ## AI Providers
220
204
 
221
- 12 providers are supported. Set your provider in `a11y.config.json` or override per-run with `--provider <name>`.
205
+ 12 providers supported. Configure once in `a11y.config.json`, or override per-run with `--provider`.
222
206
 
223
- | Provider | `--provider` name | Default model | API key source |
207
+ | Provider | `--provider` | Default model | Notes |
224
208
  |---|---|---|---|
225
- | Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | [aistudio.google.com](https://aistudio.google.com) free tier |
226
- | OpenAI | `openai` | `gpt-4o-mini` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
227
- | Anthropic | `anthropic` | `claude-sonnet-4-6` | [console.anthropic.com](https://console.anthropic.com) |
228
- | Mistral | `mistral` | `mistral-large-latest` | [console.mistral.ai](https://console.mistral.ai) |
229
- | Groq | `groq` | `llama-3.3-70b-versatile` | [console.groq.com](https://console.groq.com) |
230
- | Cohere | `cohere` | `command-r-plus` | [dashboard.cohere.com](https://dashboard.cohere.com) |
231
- | xAI | `xai` | `grok-2` | [console.x.ai](https://console.x.ai) |
232
- | DeepSeek | `deepseek` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com) |
233
- | Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | [api.together.xyz](https://api.together.xyz) |
234
- | Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api) |
235
- | Azure OpenAI | `azure-openai` | *(your deployment)* | [portal.azure.com](https://portal.azure.com) |
236
- | Ollama | `ollama` | `llama3` | Nonerun `ollama serve` locally |
237
-
238
- All models are configurable. Change the model field in `a11y.config.json` to use any model your API key has access to. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data so you always get something actionable.
209
+ | Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | Free tier available |
210
+ | OpenAI | `openai` | `gpt-4o-mini` | |
211
+ | Anthropic | `anthropic` | `claude-sonnet-4-6` | |
212
+ | Mistral | `mistral` | `mistral-large-latest` | |
213
+ | Groq | `groq` | `llama-3.3-70b-versatile` | Fast inference |
214
+ | Cohere | `cohere` | `command-r-plus` | |
215
+ | xAI | `xai` | `grok-2` | |
216
+ | DeepSeek | `deepseek` | `deepseek-chat` | |
217
+ | Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | Open-source models |
218
+ | Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | |
219
+ | Azure OpenAI | `azure-openai` | *(your deployment)* | |
220
+ | Ollama | `ollama` | `llama3` | Localno API key |
221
+
222
+ All models are configurable. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data you always get something actionable.
239
223
 
240
224
  ---
241
225
 
242
- ## Config (`a11y.config.json`)
226
+ ## Config
243
227
 
244
- Only the fields for your active `provider` are required. This file is gitignored by default.
228
+ 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
+
230
+ ```json
231
+ {
232
+ "provider": "gemini",
233
+ "apiKey": "YOUR_GEMINI_API_KEY",
234
+ "framework": "next"
235
+ }
236
+ ```
237
+
238
+ <details>
239
+ <summary>Full config reference (all 12 providers)</summary>
245
240
 
246
241
  ```json
247
242
  {
@@ -287,11 +282,16 @@ Only the fields for your active `provider` are required. This file is gitignored
287
282
  }
288
283
  ```
289
284
 
285
+ </details>
286
+
290
287
  ---
291
288
 
292
289
  ## What it checks
293
290
 
294
- 40+ rules across 10 categories, mapped to WCAG 2.1/2.2 success criteria.
291
+ 40+ rules across 10 WCAG 2.1/2.2 categories: Text Alternatives, Color Contrast, Forms, Keyboard, ARIA, Structure, Links, Media, Tables, and Language.
292
+
293
+ <details>
294
+ <summary>Full rule list</summary>
295
295
 
296
296
  ### Text Alternatives — WCAG 1.1.1
297
297
 
@@ -394,6 +394,8 @@ Only the fields for your active `provider` are required. This file is gitignored
394
394
  | `html-lang` | serious | `<html>` must have a `lang` attribute |
395
395
  | `html-lang-valid` | serious | `lang` attribute must be a valid BCP 47 language tag |
396
396
 
397
+ </details>
398
+
397
399
  ---
398
400
 
399
401
  ## Use in CI/CD
package/dist/ai/base.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fallbackExplanation } from './fallback-explanation.js';
2
+ import { getFix, getFixCategory } from './fallback-fix.js';
2
3
  export class BaseAIProvider {
3
4
  parse(text, groups, framework) {
4
5
  try {
@@ -7,7 +8,7 @@ export class BaseAIProvider {
7
8
  return groups.map((g) => {
8
9
  const found = fixes.find((f) => f.ruleId === g.ruleId);
9
10
  return found
10
- ? { ...found, selectors: g.selectors, instanceCount: g.count }
11
+ ? { ...found, selectors: g.selectors, instanceCount: g.count, fixCategory: getFixCategory(g.ruleId) }
11
12
  : this.fallbackFix(g, framework);
12
13
  });
13
14
  }
@@ -20,18 +21,23 @@ export class BaseAIProvider {
20
21
  }
21
22
  fallbackFix(g, framework) {
22
23
  const v = g.representative;
23
- const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
24
+ const selectorList = g.selectors.map((s) => `- \`${s}\``).join('\n');
24
25
  const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
25
- const fwNote = framework ? `This project uses ${framework}. ` : '';
26
+ const fwNote = framework ? `This project uses ${framework}.\n\n` : '';
27
+ const fixInstructions = getFix(g.ruleId);
28
+ const fixSection = fixInstructions ? `\n\nHow to fix:\n${fixInstructions}` : '';
26
29
  const prompt = g.count > 1
27
- ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
28
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
30
+ ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`\`\`html\n${v.html.slice(0, 400)}\n\`\`\`${fixSection}\n\nApply this fix to all ${g.count} instances across the codebase.`
31
+ : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n\nCurrent HTML:\n\`\`\`html\n${v.html.slice(0, 400)}\n\`\`\`${fixSection}`;
32
+ const category = getFixCategory(g.ruleId);
33
+ const fixedCode = category === 'edit-element' || !category ? v.html : undefined;
29
34
  return {
30
35
  ruleId: g.ruleId,
31
36
  selectors: g.selectors,
32
37
  instanceCount: g.count,
33
38
  explanation,
34
- fixedCode: v.html,
39
+ fixedCode,
40
+ fixCategory: getFixCategory(g.ruleId),
35
41
  wcagReference: `WCAG 2.1 SC ${g.wcag}`,
36
42
  optimalPrompt: prompt,
37
43
  };
@@ -0,0 +1,126 @@
1
+ const RULE_FIX_INSTRUCTIONS = {
2
+ // Text alternatives
3
+ 'img-alt': 'Add an `alt` attribute describing the image content. Use `alt=""` for decorative images.\nExample: `<img src="photo.jpg" alt="Team photo at the 2024 company retreat">`',
4
+ 'input-image-alt': 'Add an `alt` attribute to the `<input type="image">` describing the button\'s action.\nExample: `<input type="image" src="submit.png" alt="Submit form">`',
5
+ 'svg-title': 'Add a `<title>` element as the first child of `<svg>` and add `aria-labelledby` pointing to its `id`.\nExample: `<svg aria-labelledby="chartTitle" role="img"><title id="chartTitle">Monthly sales bar chart</title>...</svg>`',
6
+ 'object-alt': 'Add descriptive fallback content inside the `<object>` tag, or use `aria-label`.\nExample: `<object data="report.pdf"><p>Download the Q4 2024 report (PDF)</p></object>`',
7
+ 'role-img-alt': 'Add `aria-label` or `aria-labelledby` to the element with `role="img"`.\nExample: `<div role="img" aria-label="Star rating: 4 out of 5"></div>`',
8
+ 'image-redundant-alt': 'Change the `alt` to `alt=""` (empty string) since the image\'s meaning is already conveyed by adjacent text — screen readers skip empty-alt images automatically.',
9
+ // Tables
10
+ 'table-headers': 'Add `<th scope="col">` for column headers and `<th scope="row">` for row headers.\nExample: `<thead><tr><th scope="col">Name</th><th scope="col">Price</th></tr></thead>`',
11
+ 'table-scope-valid': 'Change the `scope` attribute to a valid value: `"col"`, `"row"`, `"colgroup"`, or `"rowgroup"`. Remove it if the element is not a header.',
12
+ 'td-headers-attr': 'Either add matching `id` attributes to the referenced `<th>` elements, or remove the invalid `headers` attributes from the `<td>` elements.',
13
+ 'table-duplicate-name': 'Make the `<caption>` and `summary` convey different information: use `<caption>` for a short visible title and `summary` for an extended description, or remove the redundant attribute.',
14
+ // Structure
15
+ 'heading-order': 'Restructure headings so levels are never skipped. Change the offending tag to the correct level (e.g., change `<h4>` directly under `<h2>` to `<h3>`). Use CSS for visual sizing — never pick heading levels for appearance.',
16
+ 'page-title': 'Add or update `<title>` in `<head>` with a unique, descriptive title.\nUse the format: `<title>Page Name — Site Name</title>`',
17
+ 'landmark-one-main': 'Wrap primary page content in exactly one `<main id="main-content">` element. Remove any duplicate `<main>` elements.',
18
+ 'list-structure': 'Replace non-semantic markup with proper list structure.\nExample: change `<div class="item">` repeated items into `<ul><li>...</li><li>...</li></ul>` for unordered, or `<ol>` for ordered lists.',
19
+ 'region-landmark': 'Wrap the content in the appropriate landmark element: `<header>`, `<nav aria-label="...">`, `<main>`, `<aside aria-label="...">`, or `<footer>`. For generic sections: `<section aria-label="Section name">`.',
20
+ 'duplicate-id': 'Make each `id` unique across the page. If the ID is referenced by `aria-labelledby` or `aria-describedby`, update those references to match the new unique IDs. Append a suffix like `-nav`, `-footer`, or a number to disambiguate.',
21
+ 'frame-title': 'Add a `title` attribute to the `<iframe>` describing its content.\nExample: `<iframe title="Product demo video" src="..."></iframe>`',
22
+ 'meta-viewport': 'Remove `user-scalable=no` and ensure `maximum-scale` is not below 5.\nChange to: `<meta name="viewport" content="width=device-width, initial-scale=1">`',
23
+ 'marquee': 'Replace `<marquee>` with a CSS-animated element. Respect `prefers-reduced-motion`:\n`@media (prefers-reduced-motion: reduce) { .marquee { animation: none; } }`\nProvide static fallback content for motion-sensitive users.',
24
+ 'p-as-heading': 'Replace the visually bold `<p>` with the appropriate heading tag (`<h2>`, `<h3>`, etc.) matching its position in the document outline. Apply visual styles via CSS, not `<b>` or `<strong>` tags inside `<p>`.',
25
+ // Media
26
+ 'video-captions': 'Add a `<track kind="captions">` element inside the `<video>` pointing to a WebVTT (.vtt) file.\nExample: `<track kind="captions" src="/captions-en.vtt" srclang="en" label="English" default>`',
27
+ 'audio-description': 'Add a `<track kind="descriptions">` element inside `<video>` for audio descriptions.\nExample: `<track kind="descriptions" src="/descriptions-en.vtt" srclang="en" label="Audio description">`',
28
+ 'audio-transcript': 'Provide a text transcript immediately after the `<audio>` element, either as a link or inline.\nExample: `<a href="/transcript.html">Read transcript</a>`',
29
+ // Links
30
+ 'link-name': 'Add descriptive text inside the `<a>`, or use `aria-label` to describe the destination.\nExample: `<a href="/products" aria-label="View all products">View all</a>`. Avoid "click here" or "read more".',
31
+ 'link-empty': 'Add descriptive text or `aria-label` to the empty anchor. If it is a decorative icon, add `aria-label`. If non-functional, remove the `<a>` element entirely.',
32
+ 'identical-links-different-purpose': 'Add unique `aria-label` to each link to distinguish its destination.\nExample: `<a href="/product-a" aria-label="Read more about Product A">Read more</a>` and `<a href="/product-b" aria-label="Read more about Product B">Read more</a>`',
33
+ 'link-new-window-warn': 'Add `rel="noopener noreferrer"` and warn users the link opens a new tab.\nExample: `<a href="..." target="_blank" rel="noopener noreferrer">Download PDF <span class="sr-only">(opens in new tab)</span></a>`',
34
+ // Forms
35
+ 'label-missing': 'Add a `<label>` linked via `for`/`id`, or add `aria-label` directly to the input.\nExample: `<label for="user-email">Email address</label><input id="user-email" type="email">`',
36
+ 'label-empty': 'Add descriptive text inside the existing `<label>`.\nExample: change `<label for="q"></label>` to `<label for="q">Search</label>`',
37
+ 'error-identification': 'Link error messages to their fields using `aria-describedby` and add `role="alert"` to the error container.\nExample: `<input aria-describedby="email-error"><span id="email-error" role="alert">Please enter a valid email address</span>`',
38
+ 'autocomplete': 'Add the correct `autocomplete` attribute to the input. Common values: `name`, `email`, `tel`, `current-password`, `new-password`, `given-name`, `family-name`, `street-address`, `postal-code`.\nExample: `<input type="email" autocomplete="email">`',
39
+ 'input-button-name': 'Add a descriptive `value` to `<input type="button/submit/reset">` or `aria-label` to icon buttons.\nExample: `<input type="submit" value="Submit registration form">` or `<button aria-label="Search"><svg aria-hidden="true">...</svg></button>`',
40
+ 'fieldset-legend': 'Wrap related controls in `<fieldset>` with `<legend>` as its first child.\nExample: `<fieldset><legend>Shipping address</legend><label>...</label><input>...</fieldset>`',
41
+ 'form-field-required-label': 'Add `required` and `aria-required="true"` to the input. Indicate required status in the label text.\nExample: `<label for="name">Full name <span aria-hidden="true">*</span><span class="sr-only">(required)</span></label><input id="name" required aria-required="true">`',
42
+ // Language
43
+ 'html-lang': 'Add a `lang` attribute to the `<html>` element with the correct BCP 47 language tag.\nExample: `<html lang="en">` for English, `<html lang="vi">` for Vietnamese, `<html lang="fr">` for French.',
44
+ 'html-lang-valid': 'Replace the invalid `lang` value with a valid BCP 47 language tag.\nValid examples: `"en"`, `"en-US"`, `"fr"`, `"de"`, `"es"`, `"zh"`, `"ja"`, `"ko"`, `"vi"`, `"ar"`.',
45
+ // Color contrast
46
+ 'color-contrast-text': 'Increase the contrast ratio between the text and background to at least 4.5:1. Darken the text color or lighten the background. Verify with the WebAIM Contrast Checker.\nExample: change `color: #999` on a white background to `color: #767676` (minimum passing value).',
47
+ 'color-contrast-large-text': 'Increase the contrast ratio to at least 3:1 for large text (18pt/24px+ regular, or 14pt/18.67px+ bold). Adjust the text or background color and verify with a contrast checker.',
48
+ // Keyboard
49
+ 'no-positive-tabindex': 'Remove the `tabindex` attribute or set it to `tabindex="0"`. Reorder DOM elements to create the correct focus sequence — never use positive tabindex values to manage tab order.',
50
+ 'interactive-not-focusable': 'Add `role="button"` (or the correct role) and `tabindex="0"`. Add keyboard event handlers for Enter and Space to match the click handler.\nExample: `<div role="button" tabindex="0" onclick="handleClick()" onkeydown="if(event.key===\'Enter\'||event.key===\' \')handleClick()">`',
51
+ 'skip-link': 'Add a skip link as the very first child of `<body>`:\n`<a href="#main-content" class="skip-link">Skip to main content</a>`\nEnsure the target exists: `<main id="main-content">` (add `id="main-content"` to your existing `<main>` element).\nAdd CSS: `.skip-link { position: absolute; left: -9999px; } .skip-link:focus { position: static; left: 0; top: 0; z-index: 9999; padding: 8px; background: #fff; color: #000; }`',
52
+ 'focus-visible': 'Remove `outline: none` or `outline: 0` from `:focus` styles. Add a clearly visible focus indicator.\nExample: `a:focus-visible, button:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; }`. Never suppress focus outlines without an equally visible replacement.',
53
+ 'scrollable-region-focusable': 'Add `tabindex="0"` to the scrollable container so keyboard users can focus and scroll it.\nExample: `<div class="overflow-auto" tabindex="0" aria-label="Scrollable content">...</div>`',
54
+ 'accesskey-unique': 'Change duplicate `accesskey` values so each one is unique across the page. If shortcuts are not needed, remove the `accesskey` attribute entirely.',
55
+ // ARIA
56
+ 'aria-valid-role': 'Remove the invalid `role` value or replace it with a valid WAI-ARIA role such as `button`, `checkbox`, `dialog`, `listbox`, `menu`, `menuitem`, `option`, `radio`, `tab`, `tabpanel`, or `tooltip`.',
57
+ 'aria-required-attr': 'Add the missing required ARIA state or property for this role.\nExamples: `role="checkbox"` needs `aria-checked`; `role="combobox"` needs `aria-expanded`; `role="slider"` needs `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`.',
58
+ 'aria-hidden-focus': 'Remove `aria-hidden="true"` from any ancestor of a focusable element, or move `aria-hidden` to a sibling that contains no focusable descendants. Never place interactive elements inside `aria-hidden="true"` containers.',
59
+ 'button-name': 'Add visible text content or `aria-label` to the `<button>`.\nExample: `<button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>` or `<button>Submit order</button>`',
60
+ 'aria-required-children': 'Add the required child elements with the correct ARIA roles inside this container.\nExamples: `role="listbox"` must contain `role="option"` children; `role="menu"` must contain `role="menuitem"` children; `role="grid"` must contain `role="row"` children.',
61
+ 'aria-required-parent': 'Move this element inside its required ARIA parent container.\nExamples: `role="option"` must be inside `role="listbox"`; `role="tab"` must be inside `role="tablist"`; `role="menuitem"` must be inside `role="menu"`.',
62
+ 'aria-prohibited-attr': 'Remove the prohibited ARIA attribute from this element. Elements with `role="presentation"` or `role="none"` must not have naming attributes like `aria-label` or `aria-labelledby`. Check the WAI-ARIA spec for the element\'s prohibited attributes.',
63
+ };
64
+ export function getFix(ruleId) {
65
+ return RULE_FIX_INSTRUCTIONS[ruleId];
66
+ }
67
+ const RULE_FIX_CATEGORY = {
68
+ // edit-element — fix targets the flagged element's own attributes or content
69
+ 'img-alt': 'edit-element',
70
+ 'input-image-alt': 'edit-element',
71
+ 'svg-title': 'edit-element',
72
+ 'object-alt': 'edit-element',
73
+ 'role-img-alt': 'edit-element',
74
+ 'image-redundant-alt': 'edit-element',
75
+ 'table-scope-valid': 'edit-element',
76
+ 'table-duplicate-name': 'edit-element',
77
+ 'page-title': 'edit-element',
78
+ 'frame-title': 'edit-element',
79
+ 'meta-viewport': 'edit-element',
80
+ 'p-as-heading': 'edit-element',
81
+ 'heading-order': 'edit-element',
82
+ 'duplicate-id': 'edit-element',
83
+ 'link-name': 'edit-element',
84
+ 'link-empty': 'edit-element',
85
+ 'identical-links-different-purpose': 'edit-element',
86
+ 'link-new-window-warn': 'edit-element',
87
+ 'label-empty': 'edit-element',
88
+ 'autocomplete': 'edit-element',
89
+ 'input-button-name': 'edit-element',
90
+ 'html-lang': 'edit-element',
91
+ 'html-lang-valid': 'edit-element',
92
+ 'no-positive-tabindex': 'edit-element',
93
+ 'interactive-not-focusable': 'edit-element',
94
+ 'scrollable-region-focusable': 'edit-element',
95
+ 'accesskey-unique': 'edit-element',
96
+ 'aria-valid-role': 'edit-element',
97
+ 'aria-required-attr': 'edit-element',
98
+ 'aria-prohibited-attr': 'edit-element',
99
+ 'button-name': 'edit-element',
100
+ 'aria-required-children': 'edit-element',
101
+ 'video-captions': 'edit-element',
102
+ 'audio-description': 'edit-element',
103
+ // add-elsewhere — fix inserts a new element at a different location
104
+ 'skip-link': 'add-elsewhere',
105
+ 'audio-transcript': 'add-elsewhere',
106
+ 'label-missing': 'add-elsewhere',
107
+ 'error-identification': 'add-elsewhere',
108
+ // change-css — fix lives in a stylesheet, not in the flagged element's markup
109
+ 'color-contrast-text': 'change-css',
110
+ 'color-contrast-large-text': 'change-css',
111
+ 'focus-visible': 'change-css',
112
+ 'marquee': 'change-css',
113
+ // restructure — fix touches multiple elements or wraps / moves content
114
+ 'table-headers': 'restructure',
115
+ 'td-headers-attr': 'restructure',
116
+ 'landmark-one-main': 'restructure',
117
+ 'list-structure': 'restructure',
118
+ 'region-landmark': 'restructure',
119
+ 'fieldset-legend': 'restructure',
120
+ 'form-field-required-label': 'restructure',
121
+ 'aria-hidden-focus': 'restructure',
122
+ 'aria-required-parent': 'restructure',
123
+ };
124
+ export function getFixCategory(ruleId) {
125
+ return RULE_FIX_CATEGORY[ruleId];
126
+ }
package/dist/ai/prompt.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getFix, getFixCategory } from './fallback-fix.js';
1
2
  const FRAMEWORK_SYNTAX = {
2
3
  'Next.js': 'React/TSX (JSX)',
3
4
  'Gatsby': 'React/JSX',
@@ -12,22 +13,28 @@ const FRAMEWORK_SYNTAX = {
12
13
  export function buildPrompt(groups, framework) {
13
14
  const syntax = framework ? (FRAMEWORK_SYNTAX[framework] ?? 'JSX/TSX') : null;
14
15
  const items = groups
15
- .map((g, i) => `${i + 1}. Rule: ${g.ruleId} | WCAG ${g.wcag} (Level ${g.level}) | Impact: ${g.impact}
16
+ .map((g, i) => {
17
+ const fixHint = getFix(g.ruleId);
18
+ const category = getFixCategory(g.ruleId) ?? 'edit-element';
19
+ const fixLine = fixHint ? `\n Fix guidance: ${fixHint}` : '';
20
+ const categoryLine = `\n Fix type: ${category}`;
21
+ return `${i + 1}. Rule: ${g.ruleId} | WCAG ${g.wcag} (Level ${g.level}) | Impact: ${g.impact}
16
22
  Page: ${g.page}
17
23
  Instances: ${g.count} element(s)
18
24
  Selectors: ${g.selectors.join(', ')}
19
25
  Representative Element: ${g.representative.html}
20
- Problem: ${g.description}`)
26
+ Problem: ${g.description}${categoryLine}${fixLine}`;
27
+ })
21
28
  .join('\n\n');
22
29
  const frameworkLine = framework
23
30
  ? `\nFramework: ${framework}. All "fixedCode" values must use ${syntax} syntax — not raw HTML.\n`
24
31
  : '';
25
32
  const fixedCodeInstruction = syntax
26
- ? `"fixedCode": the corrected snippet in ${syntax} syntax (component/template code only no imports, no surrounding boilerplate)`
27
- : `"fixedCode": the corrected HTML snippet only (no explanation, just code)`;
33
+ ? `"fixedCode": include ONLY when the violation's "Fix type" is "edit-element". Show the corrected ${syntax} snippet NOT a copy of the original broken element. Omit this field entirely for "add-elsewhere", "change-css", and "restructure" violations.`
34
+ : `"fixedCode": include ONLY when the violation's "Fix type" is "edit-element". Show the corrected HTML snippet NOT a copy of the original broken element. Omit this field entirely for "add-elsewhere", "change-css", and "restructure" violations.`;
28
35
  const optimalPromptInstruction = framework
29
- ? `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude) working in a ${framework} codebase. Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) list the affected selectors and HTML snippets, (3) state the exact change needed in ${syntax} syntax. Focus solely on the fix.`
30
- : `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) list the affected selectors and HTML snippets, (3) state the exact change needed. Focus solely on the fix.`;
36
+ ? `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude) working in a ${framework} codebase. Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) quote the affected selector and current HTML, (3) provide the exact code change required in ${syntax} syntax include a concrete before/after snippet or the specific element to add/modify/remove. The prompt must be actionable without requiring additional research.`
37
+ : `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) quote the affected selector and current HTML, (3) provide the exact code change required — include a concrete before/after snippet or the specific element to add/modify/remove. The prompt must be fully actionable: tell the developer exactly what to write, not just that something needs fixing.`;
31
38
  return `You are a WCAG accessibility expert. Analyze these violations and return a JSON array.${frameworkLine}
32
39
  Each item must have:
33
40
  - "ruleId": the rule id from the input
package/dist/cli.js CHANGED
@@ -13,13 +13,14 @@ 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.0');
16
+ .version('0.4.2');
17
17
  program
18
18
  .command('init')
19
19
  .description('Create a11y.config.json in the current directory')
20
20
  .option('--provider <name>', 'AI provider to configure (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)', 'gemini')
21
+ .option('--framework <name>', 'Your project framework — saves to config so every run uses it automatically (e.g. next, react, vue, angular, svelte, astro)')
21
22
  .action((opts) => {
22
- initConfig(opts.provider);
23
+ initConfig(opts.provider, opts.framework);
23
24
  });
24
25
  program
25
26
  .command('scan')
@@ -35,10 +36,11 @@ program
35
36
  .option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
36
37
  .option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
37
38
  .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
39
+ .option('--framework <name>', 'Override framework detection for this run (e.g. next, react, vue, angular, svelte, astro)')
38
40
  .action(async (opts) => {
39
41
  try {
40
42
  console.log(`\nScanning ${opts.url}...`);
41
- const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
43
+ const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl, framework: opts.framework });
42
44
  if (opts.terminal && !opts.fastMode) {
43
45
  printTerminalReport(result);
44
46
  }
@@ -51,10 +53,11 @@ program
51
53
  const provider = createAIProvider(config);
52
54
  const allViolations = result.pages.flatMap((p) => p.violations);
53
55
  const ruleGroups = groupViolations(allViolations, strategy);
56
+ const framework = result.framework ?? config.framework;
54
57
  if (!opts.fastMode) {
55
58
  console.log(`\nGenerating AI fixes for ${ruleGroups.length} rule groups (${allViolations.length} violations)...`);
56
59
  }
57
- const fixes = await provider.generateFixes(allViolations, strategy, result.framework);
60
+ const fixes = await provider.generateFixes(allViolations, strategy, framework);
58
61
  if (opts.terminal) {
59
62
  printAIPrompts(fixes, { explain: opts.explain, fastMode: opts.fastMode });
60
63
  }
@@ -83,6 +86,7 @@ program
83
86
  .option('--from-report [path]', 'Use an existing report instead of scanning (default: a11y-report.md)')
84
87
  .option('--apply', 'Write fixes to source files (default: dry-run, shows diff only)', false)
85
88
  .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
89
+ .option('--framework <name>', 'Override framework detection for this run (e.g. next, react, vue, angular, svelte, astro)')
86
90
  .action(async (opts) => {
87
91
  if (!opts.url && !opts.fromReport) {
88
92
  console.error('\nError: provide --url <url> to scan, or --from-report [path] to load an existing report.');
@@ -104,6 +108,7 @@ program
104
108
  apply: opts.apply,
105
109
  provider,
106
110
  srcDir: resolve(process.cwd(), 'src'),
111
+ framework: opts.framework ?? config.framework,
107
112
  });
108
113
  }
109
114
  catch (err) {
package/dist/config.js CHANGED
@@ -82,13 +82,14 @@ const STARTER_CONFIGS = {
82
82
  `Created ${CONFIG_FILE} — fill in your Azure OpenAI endpoint, deployment, and API key from https://portal.azure.com`,
83
83
  ],
84
84
  };
85
- export function initConfig(provider = 'gemini') {
85
+ export function initConfig(provider = 'gemini', framework) {
86
86
  const configPath = join(process.cwd(), CONFIG_FILE);
87
87
  if (existsSync(configPath)) {
88
88
  console.log(`${CONFIG_FILE} already exists.`);
89
89
  return;
90
90
  }
91
91
  const [starter, message] = STARTER_CONFIGS[provider];
92
- writeFileSync(configPath, JSON.stringify(starter, null, 2));
92
+ const config = framework ? { ...starter, framework } : starter;
93
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
93
94
  console.log(message);
94
95
  }
package/dist/crawler.js CHANGED
@@ -61,7 +61,7 @@ export async function crawl(options) {
61
61
  }
62
62
  }
63
63
  const results = [];
64
- let framework;
64
+ let framework = options.framework;
65
65
  for (const pageUrl of pagesToVisit) {
66
66
  const page = await context.newPage();
67
67
  try {
package/dist/fixer.js CHANGED
@@ -18,11 +18,12 @@ export async function runFix(opts) {
18
18
  console.log(chalk.green('\nNo violations found in report.'));
19
19
  return;
20
20
  }
21
+ framework = opts.framework;
21
22
  console.log(`Found ${allViolations.length} violation(s) in report. Locating source files...\n`);
22
23
  }
23
24
  else {
24
25
  console.log(`\nScanning ${opts.url}...`);
25
- const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
26
+ const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl, framework: opts.framework });
26
27
  framework = result.framework;
27
28
  if (result.totalViolations === 0) {
28
29
  console.log(chalk.green('\nNo violations found.'));
@@ -74,7 +74,11 @@ function buildFullReport(result, fixes) {
74
74
  lines.push('');
75
75
  }
76
76
  if (fix) {
77
- lines.push('**Why it matters:**', fix.explanation, '', '**Fixed code:**', '```html', fix.fixedCode, '```', '', '**📋 Prompt for your AI assistant (Cursor / Copilot / Claude):**', '```', fix.optimalPrompt, '```', '');
77
+ lines.push('**Why it matters:**', fix.explanation, '');
78
+ if ((!fix.fixCategory || fix.fixCategory === 'edit-element') && fix.fixedCode) {
79
+ lines.push('**Fixed code:**', '```html', fix.fixedCode, '```', '');
80
+ }
81
+ lines.push('**📋 Prompt for your AI assistant (Cursor / Copilot / Claude):**', '```', fix.optimalPrompt, '```', '');
78
82
  }
79
83
  lines.push('---', '');
80
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wcag-a11y",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
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.",
5
5
  "keywords": [
6
6
  "wcag",