@skilly-hand/skilly-hand 0.6.1 → 0.7.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/CHANGELOG.md +15 -0
- package/README.md +1 -0
- package/catalog/README.md +1 -0
- package/catalog/catalog-index.json +1 -0
- package/catalog/skills/accessibility-audit/SKILL.md +154 -0
- package/catalog/skills/accessibility-audit/manifest.json +37 -0
- package/catalog/skills/accessibility-audit/references/w3c-wcag22-checklist.md +80 -0
- package/catalog/skills/accessibility-audit/scripts/audit-a11y.sh +372 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,21 @@ All notable changes to this project are documented in this file.
|
|
|
16
16
|
### Removed
|
|
17
17
|
- _None._
|
|
18
18
|
|
|
19
|
+
## [0.7.0] - 2026-04-04
|
|
20
|
+
[View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.7.0)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- _None._
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- _None._
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- _None._
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
- _None._
|
|
33
|
+
|
|
19
34
|
## [0.6.1] - 2026-04-04
|
|
20
35
|
[View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.6.1)
|
|
21
36
|
|
package/README.md
CHANGED
package/catalog/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Published portable skills consumed by the `skilly-hand` CLI.
|
|
|
4
4
|
|
|
5
5
|
| Skill | Description | Tags | Installs For |
|
|
6
6
|
| ----- | ----------- | ---- | ------------ |
|
|
7
|
+
| `accessibility-audit` | Audit web accessibility against W3C WCAG 2.2 Level AA using framework-agnostic checks, remediation patterns, and portable command-line scanning. | frontend, accessibility, workflow, quality | all |
|
|
7
8
|
| `agents-root-orchestrator` | Author root AGENTS.md as a Where/What/When orchestrator that routes tasks and skill invocation clearly. | core, workflow, orchestration | all |
|
|
8
9
|
| `angular-guidelines` | Guide Angular code generation and review using latest stable Angular verification and modern framework best practices. | angular, frontend, workflow, best-practices | all |
|
|
9
10
|
| `figma-mcp-0to1` | Guide users from Figma MCP installation and authentication through first canvas creation, with function-level tool coverage and operational recovery patterns. | figma, mcp, workflow, design | all |
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Accessibility Audit Guide
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
|
|
5
|
+
Use this skill when:
|
|
6
|
+
|
|
7
|
+
- Auditing components or pages for WCAG conformance.
|
|
8
|
+
- Reviewing pull requests that change templates, interactive UI, forms, or styles.
|
|
9
|
+
- Defining accessibility acceptance criteria for frontend delivery.
|
|
10
|
+
- Converting automated scanner findings into prioritized remediations.
|
|
11
|
+
|
|
12
|
+
Do not use this skill for:
|
|
13
|
+
|
|
14
|
+
- Product-specific visual token compliance.
|
|
15
|
+
- Framework-only code style reviews unrelated to accessibility behavior.
|
|
16
|
+
- Non-web formats that need a dedicated standard beyond WCAG web content checks.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Baseline and Sources
|
|
21
|
+
|
|
22
|
+
Default baseline:
|
|
23
|
+
|
|
24
|
+
- **WCAG 2.2 Level AA**.
|
|
25
|
+
|
|
26
|
+
W3C status notes (verified from W3C WCAG overview):
|
|
27
|
+
|
|
28
|
+
- WCAG 2.2 was published on **5 October 2023** and updated on **12 December 2024**.
|
|
29
|
+
- W3C encourages using the latest WCAG version.
|
|
30
|
+
|
|
31
|
+
Use only W3C sources for decisions and remediation rationale.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Critical Patterns
|
|
36
|
+
|
|
37
|
+
### Pattern 1: Audit in POUR Order
|
|
38
|
+
|
|
39
|
+
Audit checks in this order to reduce misses:
|
|
40
|
+
|
|
41
|
+
1. **Perceivable**: text alternatives, structure, contrast.
|
|
42
|
+
2. **Operable**: keyboard, focus, target size, predictable interaction.
|
|
43
|
+
3. **Understandable**: labels, errors, language, clear behavior.
|
|
44
|
+
4. **Robust**: semantic roles/states and assistive-technology compatibility.
|
|
45
|
+
|
|
46
|
+
### Pattern 2: Prefer Native Semantics First
|
|
47
|
+
|
|
48
|
+
- Use native controls (`button`, `a`, `input`, `select`, `textarea`) before ARIA-heavy custom widgets.
|
|
49
|
+
- If custom widgets are necessary, define role, keyboard behavior, name, state, and relationship.
|
|
50
|
+
- Never remove focus indicators without a visible replacement.
|
|
51
|
+
|
|
52
|
+
### Pattern 3: Prioritize by User Impact
|
|
53
|
+
|
|
54
|
+
Fix in this order:
|
|
55
|
+
|
|
56
|
+
1. Keyboard and focus blockers.
|
|
57
|
+
2. Missing names/labels for controls and media.
|
|
58
|
+
3. Form errors and status announcements.
|
|
59
|
+
4. Contrast and non-text contrast issues.
|
|
60
|
+
|
|
61
|
+
### Pattern 4: Validate with W3C Tooling
|
|
62
|
+
|
|
63
|
+
Use W3C validators as baseline technical checks, then complete manual WCAG behavior review:
|
|
64
|
+
|
|
65
|
+
- Nu HTML Checker
|
|
66
|
+
- CSS Validator
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Decision Tree
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
Is this an interactive control? -> Verify keyboard access + visible focus + accessible name
|
|
74
|
+
Is this non-text content (image/icon/media)? -> Verify text alternative strategy
|
|
75
|
+
Is this a form input or validation message? -> Verify labels, instructions, errors, and status messaging
|
|
76
|
+
Is this a custom widget pattern? -> Verify role/state/property model and keyboard model
|
|
77
|
+
Does styling reduce legibility or discernibility? -> Verify text + non-text contrast and target size
|
|
78
|
+
Otherwise -> Run checklist sweep and document residual risk
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Code Examples
|
|
84
|
+
|
|
85
|
+
### Example 1: Icon Button with Accessible Name and Focus Indicator
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<button type="button" aria-label="Close dialog" class="icon-button">
|
|
89
|
+
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">
|
|
90
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
91
|
+
</svg>
|
|
92
|
+
</button>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
.icon-button:focus-visible {
|
|
97
|
+
outline: 2px solid #005a9c;
|
|
98
|
+
outline-offset: 2px;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Example 2: Labeled Input with Error Association
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<label for="email">Email address</label>
|
|
106
|
+
<input
|
|
107
|
+
id="email"
|
|
108
|
+
type="email"
|
|
109
|
+
aria-invalid="true"
|
|
110
|
+
aria-describedby="email-error"
|
|
111
|
+
/>
|
|
112
|
+
<p id="email-error" role="alert">Enter a valid email address.</p>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Example 3: Semantic Click Target Instead of Generic Container
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<button type="button" class="card-action">Open details</button>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<div role="button" tabindex="0" aria-label="Open details"></div>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Commands
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Run default scan on current directory
|
|
131
|
+
bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh
|
|
132
|
+
|
|
133
|
+
# Scan a specific path
|
|
134
|
+
bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh src
|
|
135
|
+
|
|
136
|
+
# Generate markdown report
|
|
137
|
+
bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh --report src
|
|
138
|
+
|
|
139
|
+
# Generate JSON output for CI pipelines
|
|
140
|
+
bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh --json src
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Resources
|
|
146
|
+
|
|
147
|
+
- Full checklist: [references/w3c-wcag22-checklist.md](references/w3c-wcag22-checklist.md)
|
|
148
|
+
- WCAG overview (WAI): https://www.w3.org/WAI/standards-guidelines/wcag/
|
|
149
|
+
- WCAG 2.2 Recommendation: https://www.w3.org/TR/WCAG22/
|
|
150
|
+
- How to Meet WCAG 2 (Quick Reference): https://www.w3.org/WAI/WCAG22/quickref/
|
|
151
|
+
- Understanding WCAG 2: https://www.w3.org/WAI/WCAG22/Understanding/
|
|
152
|
+
- W3C standards context: https://www.w3.org/standards/
|
|
153
|
+
- W3C validators and tools: https://www.w3.org/developers/tools/
|
|
154
|
+
- W3C homepage: https://www.w3.org/
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "accessibility-audit",
|
|
3
|
+
"title": "Accessibility Audit",
|
|
4
|
+
"description": "Audit web accessibility against W3C WCAG 2.2 Level AA using framework-agnostic checks, remediation patterns, and portable command-line scanning.",
|
|
5
|
+
"portable": true,
|
|
6
|
+
"tags": ["frontend", "accessibility", "workflow", "quality"],
|
|
7
|
+
"detectors": ["always"],
|
|
8
|
+
"detectionTriggers": ["manual"],
|
|
9
|
+
"installsFor": ["all"],
|
|
10
|
+
"agentSupport": ["codex", "claude", "cursor", "gemini", "copilot"],
|
|
11
|
+
"skillMetadata": {
|
|
12
|
+
"author": "skilly-hand",
|
|
13
|
+
"last-edit": "2026-04-04",
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"changelog": "Added portable WCAG 2.2 Level AA accessibility auditing skill with W3C-only references and scanner script; enables consistent web accessibility review across frameworks; affects catalog skill coverage and install plans for stacks recommending accessibility-audit",
|
|
17
|
+
"auto-invoke": "Auditing, reviewing, or implementing web accessibility against WCAG 2.2 Level AA",
|
|
18
|
+
"allowed-tools": [
|
|
19
|
+
"Read",
|
|
20
|
+
"Edit",
|
|
21
|
+
"Write",
|
|
22
|
+
"Glob",
|
|
23
|
+
"Grep",
|
|
24
|
+
"Bash",
|
|
25
|
+
"WebFetch",
|
|
26
|
+
"WebSearch",
|
|
27
|
+
"Task",
|
|
28
|
+
"SubAgent"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
{ "path": "SKILL.md", "kind": "instruction" },
|
|
33
|
+
{ "path": "references/w3c-wcag22-checklist.md", "kind": "reference" },
|
|
34
|
+
{ "path": "scripts/audit-a11y.sh", "kind": "asset" }
|
|
35
|
+
],
|
|
36
|
+
"dependencies": []
|
|
37
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# W3C WCAG 2.2 Level AA Checklist (Web)
|
|
2
|
+
|
|
3
|
+
Source basis:
|
|
4
|
+
|
|
5
|
+
- WCAG 2 Overview: https://www.w3.org/WAI/standards-guidelines/wcag/
|
|
6
|
+
- WCAG 2.2 Recommendation: https://www.w3.org/TR/WCAG22/
|
|
7
|
+
- Quick Reference: https://www.w3.org/WAI/WCAG22/quickref/
|
|
8
|
+
- Understanding WCAG 2: https://www.w3.org/WAI/WCAG22/Understanding/
|
|
9
|
+
|
|
10
|
+
Default conformance target in this skill: **WCAG 2.2 Level AA**.
|
|
11
|
+
|
|
12
|
+
## Principle 1: Perceivable
|
|
13
|
+
|
|
14
|
+
| SC | Level | Audit Focus |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| 1.1.1 Non-text Content | A | Images/icons/media alternatives (`alt`, labels, text alternatives) |
|
|
17
|
+
| 1.3.1 Info and Relationships | A | Semantic headings, labels, table/form relationships |
|
|
18
|
+
| 1.3.2 Meaningful Sequence | A | DOM reading order matches meaning |
|
|
19
|
+
| 1.3.3 Sensory Characteristics | A | Instructions are not only shape/color/position |
|
|
20
|
+
| 1.3.4 Orientation | AA | Content works in portrait and landscape unless essential |
|
|
21
|
+
| 1.3.5 Identify Input Purpose | AA | Appropriate `autocomplete` tokens for user-data fields |
|
|
22
|
+
| 1.4.1 Use of Color | A | Color is not the only information channel |
|
|
23
|
+
| 1.4.3 Contrast (Minimum) | AA | Text contrast thresholds are met |
|
|
24
|
+
| 1.4.4 Resize Text | AA | Content remains usable at 200% text resize |
|
|
25
|
+
| 1.4.10 Reflow | AA | Reflow at narrow viewport without two-dimensional scroll |
|
|
26
|
+
| 1.4.11 Non-text Contrast | AA | Component boundaries/icons/focus indicators are distinguishable |
|
|
27
|
+
| 1.4.12 Text Spacing | AA | Increased spacing does not break content/function |
|
|
28
|
+
| 1.4.13 Content on Hover or Focus | AA | Hover/focus content is dismissible, hoverable, persistent |
|
|
29
|
+
|
|
30
|
+
## Principle 2: Operable
|
|
31
|
+
|
|
32
|
+
| SC | Level | Audit Focus |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| 2.1.1 Keyboard | A | All functionality operable via keyboard |
|
|
35
|
+
| 2.1.2 No Keyboard Trap | A | Focus can enter and leave all components |
|
|
36
|
+
| 2.1.4 Character Key Shortcuts | A | Single-character shortcuts are safe/controllable |
|
|
37
|
+
| 2.4.1 Bypass Blocks | A | Mechanism exists to bypass repeated blocks |
|
|
38
|
+
| 2.4.2 Page Titled | A | Document/page has descriptive title |
|
|
39
|
+
| 2.4.3 Focus Order | A | Focus sequence follows meaningful operation |
|
|
40
|
+
| 2.4.4 Link Purpose (In Context) | A | Link purpose is clear from text/context |
|
|
41
|
+
| 2.4.6 Headings and Labels | AA | Headings/labels clearly describe purpose |
|
|
42
|
+
| 2.4.7 Focus Visible | AA | Visible focus indicator for keyboard users |
|
|
43
|
+
| 2.4.11 Focus Not Obscured (Minimum) | AA | Focused element is not fully hidden |
|
|
44
|
+
| 2.5.3 Label in Name | A | Accessible name includes visible label text |
|
|
45
|
+
| 2.5.7 Dragging Movements | AA | Drag interactions have non-drag alternatives |
|
|
46
|
+
| 2.5.8 Target Size (Minimum) | AA | Pointer targets meet minimum size/spacing |
|
|
47
|
+
|
|
48
|
+
## Principle 3: Understandable
|
|
49
|
+
|
|
50
|
+
| SC | Level | Audit Focus |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| 3.1.1 Language of Page | A | Correct `lang` on root document |
|
|
53
|
+
| 3.1.2 Language of Parts | AA | Language changes are marked in content |
|
|
54
|
+
| 3.2.1 On Focus | A | Focus alone does not trigger unexpected context change |
|
|
55
|
+
| 3.2.2 On Input | A | Input alone does not trigger unexpected context change |
|
|
56
|
+
| 3.2.3 Consistent Navigation | AA | Repeated navigation mechanisms are consistent |
|
|
57
|
+
| 3.2.4 Consistent Identification | AA | Same functions are identified consistently |
|
|
58
|
+
| 3.2.6 Consistent Help | AA | Repeated help mechanisms are consistently positioned |
|
|
59
|
+
| 3.3.1 Error Identification | A | Errors are identified in text |
|
|
60
|
+
| 3.3.2 Labels or Instructions | A | Inputs include labels/instructions |
|
|
61
|
+
| 3.3.3 Error Suggestion | AA | Suggested correction is provided when possible |
|
|
62
|
+
| 3.3.4 Error Prevention (Legal, Financial, Data) | AA | Reversible/confirmable/validated submissions |
|
|
63
|
+
| 3.3.7 Redundant Entry | A | Re-entry burden reduced where possible |
|
|
64
|
+
| 3.3.8 Accessible Authentication (Minimum) | AA | Authentication avoids inaccessible cognitive tests |
|
|
65
|
+
|
|
66
|
+
## Principle 4: Robust
|
|
67
|
+
|
|
68
|
+
| SC | Level | Audit Focus |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| 4.1.2 Name, Role, Value | A | UI controls expose name, role, state/value correctly |
|
|
71
|
+
| 4.1.3 Status Messages | AA | Dynamic status is conveyed programmatically |
|
|
72
|
+
|
|
73
|
+
## W3C Tooling Complements
|
|
74
|
+
|
|
75
|
+
Use W3C validators from https://www.w3.org/developers/tools/ as supporting checks:
|
|
76
|
+
|
|
77
|
+
- Nu HTML Checker
|
|
78
|
+
- CSS Validator
|
|
79
|
+
|
|
80
|
+
These checks complement, but do not replace, manual WCAG behavior verification.
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Portable accessibility audit helper aligned to WCAG 2.2 Level AA (W3C)
|
|
3
|
+
# Sources:
|
|
4
|
+
# - https://www.w3.org/WAI/standards-guidelines/wcag/
|
|
5
|
+
# - https://www.w3.org/TR/WCAG22/
|
|
6
|
+
# - https://www.w3.org/WAI/WCAG22/quickref/
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
RED='\033[0;31m'
|
|
11
|
+
YELLOW='\033[0;33m'
|
|
12
|
+
CYAN='\033[0;36m'
|
|
13
|
+
GREEN='\033[0;32m'
|
|
14
|
+
BOLD='\033[1m'
|
|
15
|
+
DIM='\033[2m'
|
|
16
|
+
RESET='\033[0m'
|
|
17
|
+
|
|
18
|
+
REPORT_MODE=false
|
|
19
|
+
JSON_MODE=false
|
|
20
|
+
TARGET=""
|
|
21
|
+
ROOT="$(pwd)"
|
|
22
|
+
REPORT_FILE="$ROOT/a11y-audit-report.md"
|
|
23
|
+
REPORT_CONTENT=""
|
|
24
|
+
JSON_ENTRIES=""
|
|
25
|
+
|
|
26
|
+
FILES_SCANNED=0
|
|
27
|
+
FILES_WITH_ISSUES=0
|
|
28
|
+
TOTAL_ISSUES=0
|
|
29
|
+
ISSUES_CRITICAL=0
|
|
30
|
+
ISSUES_SERIOUS=0
|
|
31
|
+
ISSUES_MODERATE=0
|
|
32
|
+
|
|
33
|
+
usage() {
|
|
34
|
+
cat <<USAGE
|
|
35
|
+
Usage:
|
|
36
|
+
bash catalog/skills/accessibility-audit/scripts/audit-a11y.sh [options] [path]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--report Write markdown report to ./a11y-audit-report.md
|
|
40
|
+
--json Emit JSON findings to stdout
|
|
41
|
+
--help Show this help
|
|
42
|
+
|
|
43
|
+
Default path:
|
|
44
|
+
current directory
|
|
45
|
+
USAGE
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
while [[ $# -gt 0 ]]; do
|
|
49
|
+
case "$1" in
|
|
50
|
+
--report) REPORT_MODE=true; shift ;;
|
|
51
|
+
--json) JSON_MODE=true; shift ;;
|
|
52
|
+
--help|-h) usage; exit 0 ;;
|
|
53
|
+
*) TARGET="$1"; shift ;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
TARGET="${TARGET:-.}"
|
|
58
|
+
|
|
59
|
+
if [[ ! -e "$TARGET" ]]; then
|
|
60
|
+
echo "Target does not exist: $TARGET" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
json_escape() {
|
|
65
|
+
echo "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g; s/\n/\\n/g'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
record_finding() {
|
|
69
|
+
local file="$1"
|
|
70
|
+
local relative_path="$2"
|
|
71
|
+
local category="$3"
|
|
72
|
+
local severity="$4"
|
|
73
|
+
local wcag_sc="$5"
|
|
74
|
+
local lineno="$6"
|
|
75
|
+
local raw_line="$7"
|
|
76
|
+
local message="$8"
|
|
77
|
+
|
|
78
|
+
local color_code=""
|
|
79
|
+
local label=""
|
|
80
|
+
|
|
81
|
+
case "$severity" in
|
|
82
|
+
critical) color_code="$RED"; label="CRITICAL"; ISSUES_CRITICAL=$((ISSUES_CRITICAL + 1)) ;;
|
|
83
|
+
serious) color_code="$YELLOW"; label="SERIOUS"; ISSUES_SERIOUS=$((ISSUES_SERIOUS + 1)) ;;
|
|
84
|
+
moderate) color_code="$CYAN"; label="MODERATE"; ISSUES_MODERATE=$((ISSUES_MODERATE + 1)) ;;
|
|
85
|
+
esac
|
|
86
|
+
|
|
87
|
+
if ! $JSON_MODE; then
|
|
88
|
+
printf " ${color_code}%-14s${RESET} [${DIM}%s${RESET}] line %-4s %s\n" "$category" "$wcag_sc" "$lineno:" "$message"
|
|
89
|
+
echo -e " ${DIM}->${RESET} $raw_line"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if $REPORT_MODE; then
|
|
93
|
+
REPORT_CONTENT+="| \`${relative_path}\` | ${lineno} | ${label} | \`${category}\` | ${wcag_sc} | ${message} |"$'\n'
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if $JSON_MODE; then
|
|
97
|
+
[[ -n "$JSON_ENTRIES" ]] && JSON_ENTRIES+=","
|
|
98
|
+
JSON_ENTRIES+="{\"file\":\"$(json_escape "$relative_path")\",\"line\":${lineno},\"severity\":\"${severity}\",\"category\":\"$(json_escape "$category")\",\"wcag\":\"$(json_escape "$wcag_sc")\",\"message\":\"$(json_escape "$message")\",\"code\":\"$(json_escape "$raw_line")\"}"
|
|
99
|
+
fi
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
audit_markup_file() {
|
|
103
|
+
local file="$1"
|
|
104
|
+
local relative_path="${file#$ROOT/}"
|
|
105
|
+
local file_issues=0
|
|
106
|
+
|
|
107
|
+
FILES_SCANNED=$((FILES_SCANNED + 1))
|
|
108
|
+
|
|
109
|
+
while IFS=: read -r lineno line; do
|
|
110
|
+
[[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
|
|
111
|
+
local trimmed
|
|
112
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
113
|
+
record_finding "$file" "$relative_path" "MISSING-ALT" "critical" "1.1.1" "$lineno" "$trimmed" "Image is missing alt attribute"
|
|
114
|
+
file_issues=$((file_issues + 1))
|
|
115
|
+
done < <(grep -n -E '<img[[:space:]]' "$file" 2>/dev/null | grep -v -E 'alt=' || true)
|
|
116
|
+
|
|
117
|
+
while IFS=: read -r lineno line; do
|
|
118
|
+
[[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
|
|
119
|
+
echo "$line" | grep -qE 'aria-label|aria-labelledby|title=' && continue
|
|
120
|
+
local trimmed
|
|
121
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
122
|
+
if echo "$trimmed" | grep -qE '<button[^>]*>[[:space:]]*</button>' || echo "$trimmed" | grep -qE '<button[^>]*/>'; then
|
|
123
|
+
record_finding "$file" "$relative_path" "EMPTY-BUTTON" "critical" "4.1.2" "$lineno" "$trimmed" "Button has no text or accessible name"
|
|
124
|
+
file_issues=$((file_issues + 1))
|
|
125
|
+
fi
|
|
126
|
+
done < <(grep -n -E '<button' "$file" 2>/dev/null || true)
|
|
127
|
+
|
|
128
|
+
while IFS=: read -r lineno line; do
|
|
129
|
+
[[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
|
|
130
|
+
echo "$line" | grep -qE 'aria-label|aria-labelledby|title=' && continue
|
|
131
|
+
local trimmed
|
|
132
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
133
|
+
if echo "$trimmed" | grep -qE '<a[^>]*>[[:space:]]*</a>'; then
|
|
134
|
+
record_finding "$file" "$relative_path" "EMPTY-LINK" "critical" "2.4.4" "$lineno" "$trimmed" "Link has no text or accessible name"
|
|
135
|
+
file_issues=$((file_issues + 1))
|
|
136
|
+
fi
|
|
137
|
+
done < <(grep -n -E '<a[[:space:]]' "$file" 2>/dev/null || true)
|
|
138
|
+
|
|
139
|
+
while IFS=: read -r lineno line; do
|
|
140
|
+
[[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
|
|
141
|
+
echo "$line" | grep -qE 'aria-label|aria-labelledby' && continue
|
|
142
|
+
echo "$line" | grep -qE 'type="hidden"|type="submit"|type="button"' && continue
|
|
143
|
+
|
|
144
|
+
local has_label=false
|
|
145
|
+
if echo "$line" | grep -qE 'id="[^"]+"'; then
|
|
146
|
+
local input_id
|
|
147
|
+
input_id="$(echo "$line" | sed -n 's/.*id="\([^"]*\)".*/\1/p' | head -1)"
|
|
148
|
+
if [[ -n "$input_id" ]] && grep -qE "for=\"${input_id}\"" "$file" 2>/dev/null; then
|
|
149
|
+
has_label=true
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
if [[ "$has_label" == "false" ]]; then
|
|
154
|
+
local trimmed
|
|
155
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
156
|
+
record_finding "$file" "$relative_path" "MISSING-LABEL" "serious" "1.3.1/3.3.2" "$lineno" "$trimmed" "Form control missing label association or ARIA naming"
|
|
157
|
+
file_issues=$((file_issues + 1))
|
|
158
|
+
fi
|
|
159
|
+
done < <(grep -n -E '<(input|select|textarea)[[:space:]]' "$file" 2>/dev/null || true)
|
|
160
|
+
|
|
161
|
+
while IFS=: read -r lineno line; do
|
|
162
|
+
[[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
|
|
163
|
+
echo "$line" | grep -qE 'role=|tabindex=' && continue
|
|
164
|
+
local trimmed
|
|
165
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
166
|
+
record_finding "$file" "$relative_path" "CLICK-NO-ROLE" "serious" "2.1.1" "$lineno" "$trimmed" "Non-semantic clickable element needs keyboard and role semantics"
|
|
167
|
+
file_issues=$((file_issues + 1))
|
|
168
|
+
done < <(grep -n -E '<(div|span|li)[^>]*((onclick=)|(\(click\)))' "$file" 2>/dev/null || true)
|
|
169
|
+
|
|
170
|
+
while IFS=: read -r lineno line; do
|
|
171
|
+
[[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
|
|
172
|
+
local tab_val
|
|
173
|
+
tab_val="$(echo "$line" | grep -oE 'tabindex="[0-9]+"' | grep -oE '[0-9]+' | head -1)"
|
|
174
|
+
if [[ -n "$tab_val" ]] && [[ "$tab_val" -gt 0 ]]; then
|
|
175
|
+
local trimmed
|
|
176
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
177
|
+
record_finding "$file" "$relative_path" "TABINDEX-POS" "serious" "2.4.3" "$lineno" "$trimmed" "Positive tabindex can break logical focus order"
|
|
178
|
+
file_issues=$((file_issues + 1))
|
|
179
|
+
fi
|
|
180
|
+
done < <(grep -n -E 'tabindex="[1-9]' "$file" 2>/dev/null || true)
|
|
181
|
+
|
|
182
|
+
local prev_level=0
|
|
183
|
+
while IFS=: read -r lineno line; do
|
|
184
|
+
[[ "$line" =~ ^[[:space:]]*\<\!-- ]] && continue
|
|
185
|
+
local level
|
|
186
|
+
level="$(echo "$line" | grep -oE '<h[1-6]' | grep -oE '[1-6]' | head -1)"
|
|
187
|
+
[[ -z "$level" ]] && continue
|
|
188
|
+
|
|
189
|
+
if [[ "$prev_level" -gt 0 ]] && [[ "$level" -gt $((prev_level + 1)) ]]; then
|
|
190
|
+
local trimmed
|
|
191
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
192
|
+
record_finding "$file" "$relative_path" "HEADING-SKIP" "moderate" "1.3.1" "$lineno" "$trimmed" "Heading level skipped"
|
|
193
|
+
file_issues=$((file_issues + 1))
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
prev_level="$level"
|
|
197
|
+
done < <(grep -n -E '<h[1-6]' "$file" 2>/dev/null || true)
|
|
198
|
+
|
|
199
|
+
if grep -qE '<html[[:space:]]' "$file" 2>/dev/null; then
|
|
200
|
+
if ! grep -qE '<html[^>]*lang=' "$file" 2>/dev/null; then
|
|
201
|
+
local lineno
|
|
202
|
+
lineno="$(grep -n -E '<html' "$file" | head -1 | cut -d: -f1)"
|
|
203
|
+
local trimmed
|
|
204
|
+
trimmed="$(grep -E '<html' "$file" | head -1 | sed 's/^[[:space:]]*//')"
|
|
205
|
+
record_finding "$file" "$relative_path" "MISSING-LANG" "serious" "3.1.1" "$lineno" "$trimmed" "Missing language declaration on html element"
|
|
206
|
+
file_issues=$((file_issues + 1))
|
|
207
|
+
fi
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
if [[ "$file_issues" -gt 0 ]]; then
|
|
211
|
+
FILES_WITH_ISSUES=$((FILES_WITH_ISSUES + 1))
|
|
212
|
+
TOTAL_ISSUES=$((TOTAL_ISSUES + file_issues))
|
|
213
|
+
if ! $JSON_MODE; then
|
|
214
|
+
echo -e "\n${BOLD}${relative_path}${RESET} - ${RED}${file_issues} issue(s)${RESET}"
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
audit_style_file() {
|
|
220
|
+
local file="$1"
|
|
221
|
+
local relative_path="${file#$ROOT/}"
|
|
222
|
+
local file_issues=0
|
|
223
|
+
|
|
224
|
+
FILES_SCANNED=$((FILES_SCANNED + 1))
|
|
225
|
+
|
|
226
|
+
while IFS=: read -r lineno line; do
|
|
227
|
+
[[ "$line" =~ ^[[:space:]]*/\* ]] && continue
|
|
228
|
+
|
|
229
|
+
local context_start=$((lineno > 4 ? lineno - 4 : 1))
|
|
230
|
+
local context_end=$((lineno + 8))
|
|
231
|
+
if sed -n "${context_start},${context_end}p" "$file" 2>/dev/null | grep -qE 'focus-visible'; then
|
|
232
|
+
continue
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
local trimmed
|
|
236
|
+
trimmed="$(echo "$line" | sed 's/^[[:space:]]*//')"
|
|
237
|
+
record_finding "$file" "$relative_path" "FOCUS-REMOVED" "critical" "2.4.7" "$lineno" "$trimmed" "Focus outline removed without visible focus-visible replacement"
|
|
238
|
+
file_issues=$((file_issues + 1))
|
|
239
|
+
done < <(
|
|
240
|
+
grep -n -E ':focus[[:space:]]*\{' "$file" 2>/dev/null | while IFS=: read -r ln _; do
|
|
241
|
+
local end=$((ln + 5))
|
|
242
|
+
if sed -n "${ln},${end}p" "$file" 2>/dev/null | grep -qE 'outline:[[:space:]]*(none|0)'; then
|
|
243
|
+
echo "${ln}:focus"
|
|
244
|
+
fi
|
|
245
|
+
done || true
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if [[ "$file_issues" -gt 0 ]]; then
|
|
249
|
+
FILES_WITH_ISSUES=$((FILES_WITH_ISSUES + 1))
|
|
250
|
+
TOTAL_ISSUES=$((TOTAL_ISSUES + file_issues))
|
|
251
|
+
if ! $JSON_MODE; then
|
|
252
|
+
echo -e "\n${BOLD}${relative_path}${RESET} - ${RED}${file_issues} issue(s)${RESET}"
|
|
253
|
+
fi
|
|
254
|
+
fi
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
generate_report() {
|
|
258
|
+
local generated_at
|
|
259
|
+
generated_at="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
260
|
+
|
|
261
|
+
cat > "$REPORT_FILE" <<REPORT_HEAD
|
|
262
|
+
# Accessibility Audit Report (WCAG 2.2 Level AA)
|
|
263
|
+
|
|
264
|
+
Generated: ${generated_at}
|
|
265
|
+
Target: \`${TARGET}\`
|
|
266
|
+
|
|
267
|
+
## Summary
|
|
268
|
+
|
|
269
|
+
| Metric | Count |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| Files scanned | ${FILES_SCANNED} |
|
|
272
|
+
| Files with issues | ${FILES_WITH_ISSUES} |
|
|
273
|
+
| Critical | ${ISSUES_CRITICAL} |
|
|
274
|
+
| Serious | ${ISSUES_SERIOUS} |
|
|
275
|
+
| Moderate | ${ISSUES_MODERATE} |
|
|
276
|
+
| Total issues | ${TOTAL_ISSUES} |
|
|
277
|
+
|
|
278
|
+
## Findings
|
|
279
|
+
|
|
280
|
+
| File | Line | Severity | Category | WCAG SC | Description |
|
|
281
|
+
| --- | --- | --- | --- | --- | --- |
|
|
282
|
+
REPORT_HEAD
|
|
283
|
+
|
|
284
|
+
echo "$REPORT_CONTENT" >> "$REPORT_FILE"
|
|
285
|
+
|
|
286
|
+
cat >> "$REPORT_FILE" <<REPORT_TAIL
|
|
287
|
+
|
|
288
|
+
## Source References (W3C)
|
|
289
|
+
|
|
290
|
+
- https://www.w3.org/WAI/standards-guidelines/wcag/
|
|
291
|
+
- https://www.w3.org/TR/WCAG22/
|
|
292
|
+
- https://www.w3.org/WAI/WCAG22/quickref/
|
|
293
|
+
- https://www.w3.org/WAI/WCAG22/Understanding/
|
|
294
|
+
- https://www.w3.org/developers/tools/
|
|
295
|
+
REPORT_TAIL
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if ! $JSON_MODE; then
|
|
299
|
+
echo -e "${BOLD}WCAG 2.2 Level AA accessibility audit${RESET}"
|
|
300
|
+
echo -e "Target: ${CYAN}${TARGET}${RESET}"
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
MARKUP_FILES=()
|
|
304
|
+
STYLE_FILES=()
|
|
305
|
+
|
|
306
|
+
if [[ -f "$TARGET" ]]; then
|
|
307
|
+
case "$TARGET" in
|
|
308
|
+
*.html|*.htm|*.xhtml|*.jsx|*.tsx) MARKUP_FILES+=("$TARGET") ;;
|
|
309
|
+
*.css|*.scss|*.sass|*.less) STYLE_FILES+=("$TARGET") ;;
|
|
310
|
+
esac
|
|
311
|
+
else
|
|
312
|
+
while IFS= read -r f; do MARKUP_FILES+=("$f"); done < <(
|
|
313
|
+
find "$TARGET" -type f \( -name "*.html" -o -name "*.htm" -o -name "*.xhtml" -o -name "*.jsx" -o -name "*.tsx" \) \
|
|
314
|
+
-not -path "*/node_modules/*" \
|
|
315
|
+
-not -path "*/.git/*" \
|
|
316
|
+
-not -path "*/dist/*" \
|
|
317
|
+
-not -path "*/build/*" \
|
|
318
|
+
-not -path "*/coverage/*" \
|
|
319
|
+
-not -path "*/.next/*" \
|
|
320
|
+
-not -path "*/storybook-static/*" \
|
|
321
|
+
| sort
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
while IFS= read -r f; do STYLE_FILES+=("$f"); done < <(
|
|
325
|
+
find "$TARGET" -type f \( -name "*.css" -o -name "*.scss" -o -name "*.sass" -o -name "*.less" \) \
|
|
326
|
+
-not -path "*/node_modules/*" \
|
|
327
|
+
-not -path "*/.git/*" \
|
|
328
|
+
-not -path "*/dist/*" \
|
|
329
|
+
-not -path "*/build/*" \
|
|
330
|
+
-not -path "*/coverage/*" \
|
|
331
|
+
-not -path "*/.next/*" \
|
|
332
|
+
-not -path "*/storybook-static/*" \
|
|
333
|
+
| sort
|
|
334
|
+
)
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
for file in "${MARKUP_FILES[@]:-}"; do
|
|
338
|
+
[[ -n "$file" ]] && audit_markup_file "$file"
|
|
339
|
+
done
|
|
340
|
+
|
|
341
|
+
for file in "${STYLE_FILES[@]:-}"; do
|
|
342
|
+
[[ -n "$file" ]] && audit_style_file "$file"
|
|
343
|
+
done
|
|
344
|
+
|
|
345
|
+
if $REPORT_MODE; then
|
|
346
|
+
generate_report
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
if $JSON_MODE; then
|
|
350
|
+
cat <<JSON
|
|
351
|
+
{"standard":"WCAG 2.2 Level AA","target":"$(json_escape "$TARGET")","summary":{"filesScanned":${FILES_SCANNED},"filesWithIssues":${FILES_WITH_ISSUES},"critical":${ISSUES_CRITICAL},"serious":${ISSUES_SERIOUS},"moderate":${ISSUES_MODERATE},"total":${TOTAL_ISSUES}},"findings":[${JSON_ENTRIES}]}
|
|
352
|
+
JSON
|
|
353
|
+
else
|
|
354
|
+
echo ""
|
|
355
|
+
echo -e "${BOLD}Summary${RESET}"
|
|
356
|
+
echo "Files scanned: $FILES_SCANNED"
|
|
357
|
+
echo "Files with issues: $FILES_WITH_ISSUES"
|
|
358
|
+
echo "Critical: $ISSUES_CRITICAL"
|
|
359
|
+
echo "Serious: $ISSUES_SERIOUS"
|
|
360
|
+
echo "Moderate: $ISSUES_MODERATE"
|
|
361
|
+
echo "Total: $TOTAL_ISSUES"
|
|
362
|
+
if $REPORT_MODE; then
|
|
363
|
+
echo "Report: $REPORT_FILE"
|
|
364
|
+
fi
|
|
365
|
+
if [[ "$TOTAL_ISSUES" -eq 0 ]]; then
|
|
366
|
+
echo -e "${GREEN}No issues found by this heuristic scanner.${RESET}"
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
if [[ "$TOTAL_ISSUES" -gt 0 ]]; then
|
|
371
|
+
exit 1
|
|
372
|
+
fi
|