red64-cli 0.1.0 → 0.2.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/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +89 -3
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Feedback Configuration
|
|
2
|
+
|
|
3
|
+
Project-specific commands for automated feedback during implementation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Test Commands
|
|
8
|
+
|
|
9
|
+
Commands to run tests during implementation. The agent will use these to verify code changes.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# Primary test command (REQUIRED)
|
|
13
|
+
test: npm test
|
|
14
|
+
|
|
15
|
+
# Test with coverage report
|
|
16
|
+
test_coverage: npm test -- --coverage
|
|
17
|
+
|
|
18
|
+
# Run specific test file (use {file} as placeholder)
|
|
19
|
+
test_file: npm test -- {file}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Linting Commands
|
|
25
|
+
|
|
26
|
+
Commands for code quality checks.
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
# Primary lint command
|
|
30
|
+
lint: npm run lint
|
|
31
|
+
|
|
32
|
+
# Lint with auto-fix
|
|
33
|
+
lint_fix: npm run lint -- --fix
|
|
34
|
+
|
|
35
|
+
# Type checking (if applicable)
|
|
36
|
+
type_check: npm run type-check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Development Server
|
|
42
|
+
|
|
43
|
+
Commands for starting the development server (required for UI verification).
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
# Start dev server
|
|
47
|
+
dev_server: npm run dev
|
|
48
|
+
|
|
49
|
+
# Dev server port
|
|
50
|
+
dev_port: 3000
|
|
51
|
+
|
|
52
|
+
# Dev server base URL
|
|
53
|
+
dev_url: http://localhost:3000
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## UI Verification
|
|
59
|
+
|
|
60
|
+
Settings for agent-browser UI verification.
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
# Enable UI verification for this project
|
|
64
|
+
ui_verification_enabled: true
|
|
65
|
+
|
|
66
|
+
# Default wait time after navigation (milliseconds)
|
|
67
|
+
navigation_wait: 3000
|
|
68
|
+
|
|
69
|
+
# Screenshot directory
|
|
70
|
+
screenshot_dir: /tmp/ui-captures
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Notes
|
|
76
|
+
|
|
77
|
+
- Update these commands to match your project's setup
|
|
78
|
+
- The agent reads this file to determine how to run tests and verify UI
|
|
79
|
+
- If a command doesn't apply, leave it empty or remove the line
|
|
80
|
+
- All commands should be runnable from the project root directory
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
Accessibility patterns for Next.js applications with semantic HTML, ARIA, keyboard navigation, focus management, and testing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Semantic first**: Use the right HTML element before reaching for ARIA
|
|
10
|
+
- **Keyboard complete**: Every interactive element must be operable without a mouse
|
|
11
|
+
- **Visible focus**: Focus indicators are a feature, not a bug
|
|
12
|
+
- **Test with real tools**: Automated checks catch 30% of issues; screen readers catch the rest
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Semantic HTML
|
|
17
|
+
|
|
18
|
+
### Use the Right Element
|
|
19
|
+
|
|
20
|
+
| Need | Correct Element | Not This |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Navigation | `<nav>` | `<div className="nav">` |
|
|
23
|
+
| Page sections | `<main>`, `<section>`, `<aside>` | `<div>` |
|
|
24
|
+
| Clickable action | `<button>` | `<div onClick>`, `<a href="#">` |
|
|
25
|
+
| Link to page | `<a href="/path">` | `<button onClick={navigate}>` |
|
|
26
|
+
| List of items | `<ul>` / `<ol>` with `<li>` | Nested `<div>` |
|
|
27
|
+
| Form field label | `<label htmlFor="id">` | `<span>` before input |
|
|
28
|
+
| Table data | `<table>` with `<thead>`, `<tbody>` | Grid of `<div>` |
|
|
29
|
+
| Heading hierarchy | `<h1>` through `<h6>` | `<div className="heading">` |
|
|
30
|
+
|
|
31
|
+
### Page Structure
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// app/dashboard/page.tsx
|
|
35
|
+
export default async function DashboardPage() {
|
|
36
|
+
return (
|
|
37
|
+
<main>
|
|
38
|
+
<h1>Dashboard</h1>
|
|
39
|
+
|
|
40
|
+
<section aria-labelledby="stats-heading">
|
|
41
|
+
<h2 id="stats-heading">Statistics</h2>
|
|
42
|
+
<StatsGrid />
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<section aria-labelledby="recent-heading">
|
|
46
|
+
<h2 id="recent-heading">Recent Activity</h2>
|
|
47
|
+
<ActivityFeed />
|
|
48
|
+
</section>
|
|
49
|
+
</main>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Heading Hierarchy
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// GOOD: Sequential heading levels
|
|
58
|
+
<h1>User Settings</h1>
|
|
59
|
+
<h2>Profile</h2>
|
|
60
|
+
<h3>Avatar</h3>
|
|
61
|
+
<h3>Display Name</h3>
|
|
62
|
+
<h2>Notifications</h2>
|
|
63
|
+
<h3>Email Preferences</h3>
|
|
64
|
+
|
|
65
|
+
// BAD: Skipped heading levels
|
|
66
|
+
<h1>User Settings</h1>
|
|
67
|
+
<h4>Profile</h4> // Skipped h2 and h3
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## ARIA Patterns
|
|
73
|
+
|
|
74
|
+
### When to Use ARIA
|
|
75
|
+
|
|
76
|
+
1. Use semantic HTML first
|
|
77
|
+
2. Add ARIA only when HTML semantics are insufficient
|
|
78
|
+
3. Never use ARIA to fix broken HTML structure
|
|
79
|
+
|
|
80
|
+
### Common ARIA Attributes
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Live regions for dynamic updates
|
|
84
|
+
<div aria-live="polite" aria-atomic="true">
|
|
85
|
+
{notification && <p>{notification}</p>}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
// Loading states
|
|
89
|
+
<button disabled={isPending} aria-busy={isPending}>
|
|
90
|
+
{isPending ? "Saving..." : "Save"}
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
// Expanded/collapsed
|
|
94
|
+
<button aria-expanded={isOpen} aria-controls="menu-panel" onClick={toggle}>
|
|
95
|
+
Menu
|
|
96
|
+
</button>
|
|
97
|
+
<div id="menu-panel" hidden={!isOpen}>
|
|
98
|
+
{/* menu content */}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
// Required fields
|
|
102
|
+
<input aria-required="true" aria-invalid={!!error} aria-describedby="email-error" />
|
|
103
|
+
{error && <p id="email-error" role="alert">{error}</p>}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Dialog/Modal Pattern
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
"use client";
|
|
110
|
+
|
|
111
|
+
import { useRef, useEffect } from "react";
|
|
112
|
+
|
|
113
|
+
interface DialogProps {
|
|
114
|
+
open: boolean;
|
|
115
|
+
onClose: () => void;
|
|
116
|
+
title: string;
|
|
117
|
+
children: React.ReactNode;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function Dialog({ open, onClose, title, children }: DialogProps) {
|
|
121
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const dialog = dialogRef.current;
|
|
125
|
+
if (!dialog) return;
|
|
126
|
+
|
|
127
|
+
if (open) {
|
|
128
|
+
dialog.showModal();
|
|
129
|
+
} else {
|
|
130
|
+
dialog.close();
|
|
131
|
+
}
|
|
132
|
+
}, [open]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<dialog
|
|
136
|
+
ref={dialogRef}
|
|
137
|
+
onClose={onClose}
|
|
138
|
+
aria-labelledby="dialog-title"
|
|
139
|
+
className="rounded-xl border bg-background p-6 shadow-lg backdrop:bg-black/50"
|
|
140
|
+
>
|
|
141
|
+
<h2 id="dialog-title" className="text-lg font-semibold">{title}</h2>
|
|
142
|
+
<div className="mt-4">{children}</div>
|
|
143
|
+
<button onClick={onClose} aria-label="Close dialog" className="absolute right-4 top-4">
|
|
144
|
+
<XIcon />
|
|
145
|
+
</button>
|
|
146
|
+
</dialog>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Keyboard Navigation
|
|
154
|
+
|
|
155
|
+
### Focus Management
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
"use client";
|
|
159
|
+
|
|
160
|
+
import { useRef, useEffect } from "react";
|
|
161
|
+
|
|
162
|
+
// Auto-focus on mount (for modals, drawers)
|
|
163
|
+
export function SearchPanel({ isOpen }: { isOpen: boolean }) {
|
|
164
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (isOpen) {
|
|
168
|
+
inputRef.current?.focus();
|
|
169
|
+
}
|
|
170
|
+
}, [isOpen]);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div role="search">
|
|
174
|
+
<input ref={inputRef} type="search" placeholder="Search..." aria-label="Search" />
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Skip Navigation Link
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// app/layout.tsx
|
|
184
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
185
|
+
return (
|
|
186
|
+
<html lang="en">
|
|
187
|
+
<body>
|
|
188
|
+
<a
|
|
189
|
+
href="#main-content"
|
|
190
|
+
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-white"
|
|
191
|
+
>
|
|
192
|
+
Skip to main content
|
|
193
|
+
</a>
|
|
194
|
+
<Header />
|
|
195
|
+
<main id="main-content" tabIndex={-1}>
|
|
196
|
+
{children}
|
|
197
|
+
</main>
|
|
198
|
+
<Footer />
|
|
199
|
+
</body>
|
|
200
|
+
</html>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Keyboard Shortcuts
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// Roving tabindex for toolbar/menu
|
|
209
|
+
"use client";
|
|
210
|
+
|
|
211
|
+
import { useState, useRef } from "react";
|
|
212
|
+
|
|
213
|
+
export function Toolbar({ items }: { items: { label: string; onClick: () => void }[] }) {
|
|
214
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
215
|
+
const refs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
216
|
+
|
|
217
|
+
function handleKeyDown(e: React.KeyboardEvent, index: number) {
|
|
218
|
+
let nextIndex = index;
|
|
219
|
+
if (e.key === "ArrowRight") nextIndex = (index + 1) % items.length;
|
|
220
|
+
if (e.key === "ArrowLeft") nextIndex = (index - 1 + items.length) % items.length;
|
|
221
|
+
|
|
222
|
+
if (nextIndex !== index) {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
setActiveIndex(nextIndex);
|
|
225
|
+
refs.current[nextIndex]?.focus();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div role="toolbar" aria-label="Actions">
|
|
231
|
+
{items.map((item, i) => (
|
|
232
|
+
<button
|
|
233
|
+
key={i}
|
|
234
|
+
ref={(el) => { refs.current[i] = el; }}
|
|
235
|
+
tabIndex={i === activeIndex ? 0 : -1}
|
|
236
|
+
onKeyDown={(e) => handleKeyDown(e, i)}
|
|
237
|
+
onClick={item.onClick}
|
|
238
|
+
>
|
|
239
|
+
{item.label}
|
|
240
|
+
</button>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Next.js Specific Patterns
|
|
250
|
+
|
|
251
|
+
### Images
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import Image from "next/image";
|
|
255
|
+
|
|
256
|
+
// GOOD: Descriptive alt text
|
|
257
|
+
<Image src="/team/jane.jpg" alt="Jane Doe, CTO" width={200} height={200} />
|
|
258
|
+
|
|
259
|
+
// GOOD: Decorative image
|
|
260
|
+
<Image src="/pattern.svg" alt="" width={100} height={100} aria-hidden="true" />
|
|
261
|
+
|
|
262
|
+
// BAD
|
|
263
|
+
<Image src="/team/jane.jpg" alt="image" width={200} height={200} />
|
|
264
|
+
<Image src="/team/jane.jpg" alt="photo.jpg" width={200} height={200} />
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Links
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import Link from "next/link";
|
|
271
|
+
|
|
272
|
+
// GOOD: Descriptive link text
|
|
273
|
+
<Link href="/settings">Account settings</Link>
|
|
274
|
+
|
|
275
|
+
// GOOD: Link with context via aria-label
|
|
276
|
+
<Link href={`/users/${user.id}`} aria-label={`View profile for ${user.name}`}>
|
|
277
|
+
View profile
|
|
278
|
+
</Link>
|
|
279
|
+
|
|
280
|
+
// BAD: Ambiguous link text
|
|
281
|
+
<Link href="/settings">Click here</Link>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Dynamic Route Announcements
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// components/route-announcer.tsx
|
|
288
|
+
"use client";
|
|
289
|
+
|
|
290
|
+
import { usePathname } from "next/navigation";
|
|
291
|
+
import { useEffect, useState } from "react";
|
|
292
|
+
|
|
293
|
+
export function RouteAnnouncer() {
|
|
294
|
+
const pathname = usePathname();
|
|
295
|
+
const [announcement, setAnnouncement] = useState("");
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const pageTitle = document.title;
|
|
299
|
+
setAnnouncement(`Navigated to ${pageTitle}`);
|
|
300
|
+
}, [pathname]);
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div aria-live="assertive" aria-atomic="true" className="sr-only">
|
|
304
|
+
{announcement}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Color and Contrast
|
|
313
|
+
|
|
314
|
+
### Minimum Ratios (WCAG 2.1 AA)
|
|
315
|
+
|
|
316
|
+
| Element | Minimum Ratio |
|
|
317
|
+
|---|---|
|
|
318
|
+
| Normal text (< 18px) | 4.5:1 |
|
|
319
|
+
| Large text (>= 18px bold, >= 24px) | 3:1 |
|
|
320
|
+
| UI components and icons | 3:1 |
|
|
321
|
+
| Focus indicators | 3:1 against adjacent colors |
|
|
322
|
+
|
|
323
|
+
### Never Rely on Color Alone
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// GOOD: Color + icon + text
|
|
327
|
+
<Badge variant="error">
|
|
328
|
+
<AlertIcon className="mr-1" aria-hidden="true" />
|
|
329
|
+
Failed
|
|
330
|
+
</Badge>
|
|
331
|
+
|
|
332
|
+
// BAD: Color only
|
|
333
|
+
<span className="text-red-500">Failed</span>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Forms
|
|
339
|
+
|
|
340
|
+
### Accessible Form Pattern
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
export function LoginForm() {
|
|
344
|
+
return (
|
|
345
|
+
<form aria-labelledby="login-heading">
|
|
346
|
+
<h2 id="login-heading">Sign in to your account</h2>
|
|
347
|
+
|
|
348
|
+
<div>
|
|
349
|
+
<label htmlFor="email">Email address</label>
|
|
350
|
+
<input
|
|
351
|
+
id="email"
|
|
352
|
+
name="email"
|
|
353
|
+
type="email"
|
|
354
|
+
required
|
|
355
|
+
autoComplete="email"
|
|
356
|
+
aria-describedby="email-hint"
|
|
357
|
+
/>
|
|
358
|
+
<p id="email-hint" className="text-sm text-muted-foreground">
|
|
359
|
+
We will never share your email.
|
|
360
|
+
</p>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div>
|
|
364
|
+
<label htmlFor="password">Password</label>
|
|
365
|
+
<input
|
|
366
|
+
id="password"
|
|
367
|
+
name="password"
|
|
368
|
+
type="password"
|
|
369
|
+
required
|
|
370
|
+
autoComplete="current-password"
|
|
371
|
+
aria-invalid={!!errors.password}
|
|
372
|
+
aria-describedby={errors.password ? "password-error" : undefined}
|
|
373
|
+
/>
|
|
374
|
+
{errors.password && (
|
|
375
|
+
<p id="password-error" role="alert" className="text-sm text-destructive">
|
|
376
|
+
{errors.password}
|
|
377
|
+
</p>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<button type="submit">Sign in</button>
|
|
382
|
+
</form>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Testing
|
|
390
|
+
|
|
391
|
+
### Automated Tools
|
|
392
|
+
|
|
393
|
+
| Tool | Purpose | Integration |
|
|
394
|
+
|---|---|---|
|
|
395
|
+
| axe-core | Automated a11y checks | Vitest + @axe-core/react |
|
|
396
|
+
| Playwright | E2E a11y assertions | Built-in accessibility snapshots |
|
|
397
|
+
| eslint-plugin-jsx-a11y | Lint-time checks | ESLint config |
|
|
398
|
+
|
|
399
|
+
### Axe Integration with Vitest
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import { render } from "@testing-library/react";
|
|
403
|
+
import { axe, toHaveNoViolations } from "jest-axe";
|
|
404
|
+
|
|
405
|
+
expect.extend(toHaveNoViolations);
|
|
406
|
+
|
|
407
|
+
test("LoginForm has no accessibility violations", async () => {
|
|
408
|
+
const { container } = render(<LoginForm />);
|
|
409
|
+
const results = await axe(container);
|
|
410
|
+
expect(results).toHaveNoViolations();
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Manual Testing Checklist
|
|
415
|
+
|
|
416
|
+
- Navigate the entire page using only the keyboard (Tab, Shift+Tab, Enter, Escape, Arrow keys)
|
|
417
|
+
- Test with VoiceOver (macOS: Cmd+F5) or NVDA (Windows)
|
|
418
|
+
- Zoom to 200% and verify layout does not break
|
|
419
|
+
- Enable "Reduce Motion" in OS settings and verify animations respect it
|
|
420
|
+
- Test with forced colors mode (Windows High Contrast)
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Anti-Patterns
|
|
425
|
+
|
|
426
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
427
|
+
|---|---|---|
|
|
428
|
+
| `<div onClick>` for buttons | Not keyboard accessible, no role | Use `<button>` |
|
|
429
|
+
| Missing alt text on images | Screen readers say "image" with no context | Descriptive alt or `alt=""` for decorative |
|
|
430
|
+
| `outline: none` without replacement | Keyboard users lose their place | Use `focus-visible` with visible ring |
|
|
431
|
+
| Color-only status indicators | Invisible to color-blind users | Add icons, text, or patterns |
|
|
432
|
+
| Auto-playing media | Disorienting, cannot be stopped | Never auto-play; provide controls |
|
|
433
|
+
| `tabIndex > 0` | Breaks natural tab order | Use `tabIndex={0}` or `tabIndex={-1}` only |
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
_Accessibility is not a feature to add later. It is a quality of well-built software. If it does not work with a keyboard and a screen reader, it does not work._
|