devflow-kit 1.0.0 → 1.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/CHANGELOG.md +69 -0
- package/README.md +35 -11
- package/dist/cli.js +5 -1
- package/dist/commands/ambient.d.ts +18 -0
- package/dist/commands/ambient.js +136 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +97 -10
- package/dist/commands/memory.d.ts +22 -0
- package/dist/commands/memory.js +175 -0
- package/dist/commands/uninstall.js +72 -5
- package/dist/plugins.js +74 -3
- package/dist/utils/post-install.d.ts +12 -0
- package/dist/utils/post-install.js +82 -1
- package/dist/utils/safe-delete-install.d.ts +7 -0
- package/dist/utils/safe-delete-install.js +40 -5
- package/package.json +2 -1
- package/plugins/devflow-accessibility/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +7 -0
- package/plugins/devflow-ambient/README.md +49 -0
- package/plugins/devflow-ambient/commands/ambient.md +110 -0
- package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +89 -0
- package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +68 -0
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -4
- package/plugins/devflow-code-review/agents/reviewer.md +8 -0
- package/plugins/devflow-code-review/commands/code-review-teams.md +11 -1
- package/plugins/devflow-code-review/commands/code-review.md +12 -2
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +3 -6
- package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +10 -6
- package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +139 -0
- package/plugins/devflow-core-skills/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/skills/go/SKILL.md +187 -0
- package/plugins/devflow-go/skills/go/references/concurrency.md +312 -0
- package/plugins/devflow-go/skills/go/references/detection.md +129 -0
- package/plugins/devflow-go/skills/go/references/patterns.md +232 -0
- package/plugins/devflow-go/skills/go/references/violations.md +205 -0
- package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -3
- package/plugins/devflow-implement/agents/coder.md +11 -6
- package/plugins/devflow-java/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-java/skills/java/SKILL.md +183 -0
- package/plugins/devflow-java/skills/java/references/detection.md +120 -0
- package/plugins/devflow-java/skills/java/references/modern-java.md +270 -0
- package/plugins/devflow-java/skills/java/references/patterns.md +235 -0
- package/plugins/devflow-java/skills/java/references/violations.md +213 -0
- package/plugins/devflow-python/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-python/skills/python/SKILL.md +188 -0
- package/plugins/devflow-python/skills/python/references/async.md +220 -0
- package/plugins/devflow-python/skills/python/references/detection.md +128 -0
- package/plugins/devflow-python/skills/python/references/patterns.md +226 -0
- package/plugins/devflow-python/skills/python/references/violations.md +204 -0
- package/plugins/devflow-react/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/SKILL.md +1 -1
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/references/patterns.md +3 -3
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-rust/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-rust/skills/rust/SKILL.md +193 -0
- package/plugins/devflow-rust/skills/rust/references/detection.md +131 -0
- package/plugins/devflow-rust/skills/rust/references/ownership.md +242 -0
- package/plugins/devflow-rust/skills/rust/references/patterns.md +210 -0
- package/plugins/devflow-rust/skills/rust/references/violations.md +191 -0
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-typescript/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/patterns.md +3 -3
- package/scripts/hooks/ambient-prompt.sh +48 -0
- package/scripts/hooks/background-memory-update.sh +49 -8
- package/scripts/hooks/ensure-memory-gitignore.sh +17 -0
- package/scripts/hooks/pre-compact-memory.sh +12 -6
- package/scripts/hooks/session-start-memory.sh +50 -8
- package/scripts/hooks/stop-update-memory.sh +10 -6
- package/shared/agents/coder.md +11 -6
- package/shared/agents/reviewer.md +8 -0
- package/shared/skills/ambient-router/SKILL.md +89 -0
- package/shared/skills/ambient-router/references/skill-catalog.md +68 -0
- package/shared/skills/docs-framework/SKILL.md +10 -6
- package/shared/skills/go/SKILL.md +187 -0
- package/shared/skills/go/references/concurrency.md +312 -0
- package/shared/skills/go/references/detection.md +129 -0
- package/shared/skills/go/references/patterns.md +232 -0
- package/shared/skills/go/references/violations.md +205 -0
- package/shared/skills/java/SKILL.md +183 -0
- package/shared/skills/java/references/detection.md +120 -0
- package/shared/skills/java/references/modern-java.md +270 -0
- package/shared/skills/java/references/patterns.md +235 -0
- package/shared/skills/java/references/violations.md +213 -0
- package/shared/skills/python/SKILL.md +188 -0
- package/shared/skills/python/references/async.md +220 -0
- package/shared/skills/python/references/detection.md +128 -0
- package/shared/skills/python/references/patterns.md +226 -0
- package/shared/skills/python/references/violations.md +204 -0
- package/shared/skills/react/SKILL.md +1 -1
- package/shared/skills/react/references/patterns.md +3 -3
- package/shared/skills/rust/SKILL.md +193 -0
- package/shared/skills/rust/references/detection.md +131 -0
- package/shared/skills/rust/references/ownership.md +242 -0
- package/shared/skills/rust/references/patterns.md +210 -0
- package/shared/skills/rust/references/violations.md +191 -0
- package/shared/skills/test-driven-development/SKILL.md +139 -0
- package/shared/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/shared/skills/typescript/references/patterns.md +3 -3
- package/src/templates/managed-settings.json +14 -0
- package/plugins/devflow-code-review/skills/react/SKILL.md +0 -276
- package/plugins/devflow-code-review/skills/react/references/patterns.md +0 -1331
- package/plugins/devflow-core-skills/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-core-skills/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-core-skills/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-core-skills/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-core-skills/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-core-skills/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-core-skills/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-core-skills/skills/frontend-design/references/violations.md +0 -453
- package/plugins/devflow-core-skills/skills/react/references/violations.md +0 -565
- package/plugins/devflow-implement/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-implement/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-implement/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-implement/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-implement/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-implement/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-implement/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-implement/skills/frontend-design/references/violations.md +0 -453
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-react}/skills/react/references/violations.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/SKILL.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/violations.md +0 -0
|
@@ -1,670 +0,0 @@
|
|
|
1
|
-
# Accessibility Correct Patterns
|
|
2
|
-
|
|
3
|
-
Extended correct patterns for accessibility. Reference from main SKILL.md.
|
|
4
|
-
|
|
5
|
-
## Keyboard Navigation Patterns
|
|
6
|
-
|
|
7
|
-
### Full Keyboard Support
|
|
8
|
-
|
|
9
|
-
```tsx
|
|
10
|
-
// CORRECT: Custom interactive element with keyboard support
|
|
11
|
-
function ClickableCard({ onClick, children }) {
|
|
12
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
13
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
14
|
-
e.preventDefault();
|
|
15
|
-
onClick();
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div
|
|
21
|
-
role="button"
|
|
22
|
-
tabIndex={0}
|
|
23
|
-
onClick={onClick}
|
|
24
|
-
onKeyDown={handleKeyDown}
|
|
25
|
-
className="card"
|
|
26
|
-
>
|
|
27
|
-
{children}
|
|
28
|
-
</div>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// CORRECT: Drag with keyboard alternative
|
|
33
|
-
function DraggableItem({ item, onMove }) {
|
|
34
|
-
return (
|
|
35
|
-
<div
|
|
36
|
-
draggable
|
|
37
|
-
onDragStart={handleDrag}
|
|
38
|
-
onDragEnd={handleDrop}
|
|
39
|
-
>
|
|
40
|
-
{item.name}
|
|
41
|
-
<div className="keyboard-controls">
|
|
42
|
-
<button aria-label="Move up" onClick={() => onMove('up')}>↑</button>
|
|
43
|
-
<button aria-label="Move down" onClick={() => onMove('down')}>↓</button>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Focus Trap for Modals
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
// CORRECT: Complete focus management
|
|
54
|
-
function Modal({ isOpen, onClose, title, children }) {
|
|
55
|
-
const modalRef = useRef<HTMLDivElement>(null);
|
|
56
|
-
const previousFocus = useRef<HTMLElement | null>(null);
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (isOpen) {
|
|
60
|
-
previousFocus.current = document.activeElement as HTMLElement;
|
|
61
|
-
const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
|
|
62
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
63
|
-
);
|
|
64
|
-
firstFocusable?.focus();
|
|
65
|
-
} else {
|
|
66
|
-
previousFocus.current?.focus();
|
|
67
|
-
}
|
|
68
|
-
}, [isOpen]);
|
|
69
|
-
|
|
70
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
71
|
-
if (e.key === 'Escape') {
|
|
72
|
-
onClose();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (e.key !== 'Tab') return;
|
|
77
|
-
|
|
78
|
-
const focusable = modalRef.current?.querySelectorAll<HTMLElement>(
|
|
79
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
80
|
-
);
|
|
81
|
-
if (!focusable?.length) return;
|
|
82
|
-
|
|
83
|
-
const first = focusable[0];
|
|
84
|
-
const last = focusable[focusable.length - 1];
|
|
85
|
-
|
|
86
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
87
|
-
e.preventDefault();
|
|
88
|
-
last.focus();
|
|
89
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
90
|
-
e.preventDefault();
|
|
91
|
-
first.focus();
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
if (!isOpen) return null;
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div className="modal-overlay" onClick={onClose}>
|
|
99
|
-
<div
|
|
100
|
-
ref={modalRef}
|
|
101
|
-
role="dialog"
|
|
102
|
-
aria-modal="true"
|
|
103
|
-
aria-labelledby="modal-title"
|
|
104
|
-
onClick={(e) => e.stopPropagation()}
|
|
105
|
-
onKeyDown={handleKeyDown}
|
|
106
|
-
>
|
|
107
|
-
<h2 id="modal-title">{title}</h2>
|
|
108
|
-
{children}
|
|
109
|
-
<button onClick={onClose}>Close</button>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Custom Focus Styles
|
|
117
|
-
|
|
118
|
-
```css
|
|
119
|
-
/* CORRECT: Visible, accessible focus styles */
|
|
120
|
-
:focus-visible {
|
|
121
|
-
outline: 2px solid #005fcc;
|
|
122
|
-
outline-offset: 2px;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* Remove outline only for mouse users */
|
|
126
|
-
:focus:not(:focus-visible) {
|
|
127
|
-
outline: none;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/* High contrast focus for dark backgrounds */
|
|
131
|
-
.dark-theme :focus-visible {
|
|
132
|
-
outline: 2px solid #fff;
|
|
133
|
-
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.5);
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Roving Tab Index
|
|
138
|
-
|
|
139
|
-
```tsx
|
|
140
|
-
// CORRECT: Arrow key navigation within component
|
|
141
|
-
function RadioGroup({ options, value, onChange }) {
|
|
142
|
-
const [focusIndex, setFocusIndex] = useState(0);
|
|
143
|
-
|
|
144
|
-
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
|
|
145
|
-
let newIndex = index;
|
|
146
|
-
|
|
147
|
-
switch (e.key) {
|
|
148
|
-
case 'ArrowDown':
|
|
149
|
-
case 'ArrowRight':
|
|
150
|
-
newIndex = (index + 1) % options.length;
|
|
151
|
-
break;
|
|
152
|
-
case 'ArrowUp':
|
|
153
|
-
case 'ArrowLeft':
|
|
154
|
-
newIndex = (index - 1 + options.length) % options.length;
|
|
155
|
-
break;
|
|
156
|
-
default:
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
e.preventDefault();
|
|
161
|
-
setFocusIndex(newIndex);
|
|
162
|
-
onChange(options[newIndex].value);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
return (
|
|
166
|
-
<div role="radiogroup">
|
|
167
|
-
{options.map((option, index) => (
|
|
168
|
-
<label key={option.value}>
|
|
169
|
-
<input
|
|
170
|
-
type="radio"
|
|
171
|
-
name="radio-group"
|
|
172
|
-
value={option.value}
|
|
173
|
-
checked={value === option.value}
|
|
174
|
-
tabIndex={index === focusIndex ? 0 : -1}
|
|
175
|
-
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
176
|
-
onChange={() => onChange(option.value)}
|
|
177
|
-
/>
|
|
178
|
-
{option.label}
|
|
179
|
-
</label>
|
|
180
|
-
))}
|
|
181
|
-
</div>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## ARIA Patterns
|
|
189
|
-
|
|
190
|
-
### Icon Buttons with Labels
|
|
191
|
-
|
|
192
|
-
```tsx
|
|
193
|
-
// CORRECT: Accessible icon button
|
|
194
|
-
<button aria-label="Close dialog" onClick={onClose}>
|
|
195
|
-
<CloseIcon aria-hidden="true" />
|
|
196
|
-
</button>
|
|
197
|
-
|
|
198
|
-
// CORRECT: Icon with visible tooltip
|
|
199
|
-
<button aria-describedby="tooltip-1" onClick={onSettings}>
|
|
200
|
-
<SettingsIcon aria-hidden="true" />
|
|
201
|
-
<span id="tooltip-1" role="tooltip" className="tooltip">
|
|
202
|
-
Settings
|
|
203
|
-
</span>
|
|
204
|
-
</button>
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Live Regions
|
|
208
|
-
|
|
209
|
-
```tsx
|
|
210
|
-
// CORRECT: Status announcements
|
|
211
|
-
function SearchResults({ query, results, isLoading }) {
|
|
212
|
-
return (
|
|
213
|
-
<div>
|
|
214
|
-
<div
|
|
215
|
-
role="status"
|
|
216
|
-
aria-live="polite"
|
|
217
|
-
aria-atomic="true"
|
|
218
|
-
className="sr-only"
|
|
219
|
-
>
|
|
220
|
-
{isLoading
|
|
221
|
-
? 'Searching...'
|
|
222
|
-
: `${results.length} results found for "${query}"`}
|
|
223
|
-
</div>
|
|
224
|
-
<ul>
|
|
225
|
-
{results.map((result) => (
|
|
226
|
-
<li key={result.id}>{result.title}</li>
|
|
227
|
-
))}
|
|
228
|
-
</ul>
|
|
229
|
-
</div>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// CORRECT: Form submission feedback
|
|
234
|
-
function SubmitButton({ isSubmitting, success, error }) {
|
|
235
|
-
return (
|
|
236
|
-
<>
|
|
237
|
-
<button type="submit" disabled={isSubmitting}>
|
|
238
|
-
{isSubmitting ? 'Submitting...' : 'Submit'}
|
|
239
|
-
</button>
|
|
240
|
-
<div role="status" aria-live="assertive">
|
|
241
|
-
{success && <p>Form submitted successfully!</p>}
|
|
242
|
-
{error && <p role="alert">Error: {error}</p>}
|
|
243
|
-
</div>
|
|
244
|
-
</>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### Expandable Content
|
|
250
|
-
|
|
251
|
-
```tsx
|
|
252
|
-
// CORRECT: Accordion with proper ARIA
|
|
253
|
-
function Accordion({ items }) {
|
|
254
|
-
const [expanded, setExpanded] = useState<string | null>(null);
|
|
255
|
-
|
|
256
|
-
return (
|
|
257
|
-
<div>
|
|
258
|
-
{items.map((item) => (
|
|
259
|
-
<div key={item.id}>
|
|
260
|
-
<h3>
|
|
261
|
-
<button
|
|
262
|
-
aria-expanded={expanded === item.id}
|
|
263
|
-
aria-controls={`panel-${item.id}`}
|
|
264
|
-
onClick={() => setExpanded(expanded === item.id ? null : item.id)}
|
|
265
|
-
>
|
|
266
|
-
{item.title}
|
|
267
|
-
<ChevronIcon aria-hidden="true" />
|
|
268
|
-
</button>
|
|
269
|
-
</h3>
|
|
270
|
-
<div
|
|
271
|
-
id={`panel-${item.id}`}
|
|
272
|
-
role="region"
|
|
273
|
-
aria-labelledby={`header-${item.id}`}
|
|
274
|
-
hidden={expanded !== item.id}
|
|
275
|
-
>
|
|
276
|
-
{item.content}
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
))}
|
|
280
|
-
</div>
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## Form Patterns
|
|
288
|
-
|
|
289
|
-
### Accessible Form Field
|
|
290
|
-
|
|
291
|
-
```tsx
|
|
292
|
-
// CORRECT: Complete accessible input
|
|
293
|
-
function FormField({
|
|
294
|
-
id,
|
|
295
|
-
label,
|
|
296
|
-
type = 'text',
|
|
297
|
-
required,
|
|
298
|
-
error,
|
|
299
|
-
hint,
|
|
300
|
-
...props
|
|
301
|
-
}) {
|
|
302
|
-
const errorId = error ? `${id}-error` : undefined;
|
|
303
|
-
const hintId = hint ? `${id}-hint` : undefined;
|
|
304
|
-
const describedBy = [errorId, hintId].filter(Boolean).join(' ') || undefined;
|
|
305
|
-
|
|
306
|
-
return (
|
|
307
|
-
<div className="form-field">
|
|
308
|
-
<label htmlFor={id}>
|
|
309
|
-
{label}
|
|
310
|
-
{required && <span aria-hidden="true"> *</span>}
|
|
311
|
-
{required && <span className="sr-only"> (required)</span>}
|
|
312
|
-
</label>
|
|
313
|
-
|
|
314
|
-
{hint && (
|
|
315
|
-
<p id={hintId} className="hint">
|
|
316
|
-
{hint}
|
|
317
|
-
</p>
|
|
318
|
-
)}
|
|
319
|
-
|
|
320
|
-
<input
|
|
321
|
-
id={id}
|
|
322
|
-
type={type}
|
|
323
|
-
required={required}
|
|
324
|
-
aria-invalid={!!error}
|
|
325
|
-
aria-describedby={describedBy}
|
|
326
|
-
{...props}
|
|
327
|
-
/>
|
|
328
|
-
|
|
329
|
-
{error && (
|
|
330
|
-
<p id={errorId} role="alert" className="error">
|
|
331
|
-
{error}
|
|
332
|
-
</p>
|
|
333
|
-
)}
|
|
334
|
-
</div>
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### Accessible Select
|
|
340
|
-
|
|
341
|
-
```tsx
|
|
342
|
-
// CORRECT: Custom select with full keyboard support
|
|
343
|
-
function Select({ label, options, value, onChange }) {
|
|
344
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
345
|
-
const [focusIndex, setFocusIndex] = useState(0);
|
|
346
|
-
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
347
|
-
const listRef = useRef<HTMLUListElement>(null);
|
|
348
|
-
|
|
349
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
350
|
-
switch (e.key) {
|
|
351
|
-
case 'Enter':
|
|
352
|
-
case ' ':
|
|
353
|
-
if (isOpen) {
|
|
354
|
-
onChange(options[focusIndex].value);
|
|
355
|
-
setIsOpen(false);
|
|
356
|
-
buttonRef.current?.focus();
|
|
357
|
-
} else {
|
|
358
|
-
setIsOpen(true);
|
|
359
|
-
}
|
|
360
|
-
e.preventDefault();
|
|
361
|
-
break;
|
|
362
|
-
case 'ArrowDown':
|
|
363
|
-
if (isOpen) {
|
|
364
|
-
setFocusIndex((i) => Math.min(i + 1, options.length - 1));
|
|
365
|
-
} else {
|
|
366
|
-
setIsOpen(true);
|
|
367
|
-
}
|
|
368
|
-
e.preventDefault();
|
|
369
|
-
break;
|
|
370
|
-
case 'ArrowUp':
|
|
371
|
-
if (isOpen) {
|
|
372
|
-
setFocusIndex((i) => Math.max(i - 1, 0));
|
|
373
|
-
}
|
|
374
|
-
e.preventDefault();
|
|
375
|
-
break;
|
|
376
|
-
case 'Escape':
|
|
377
|
-
setIsOpen(false);
|
|
378
|
-
buttonRef.current?.focus();
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
const selectedOption = options.find((o) => o.value === value);
|
|
384
|
-
|
|
385
|
-
return (
|
|
386
|
-
<div className="select-wrapper" onKeyDown={handleKeyDown}>
|
|
387
|
-
<label id="select-label">{label}</label>
|
|
388
|
-
<button
|
|
389
|
-
ref={buttonRef}
|
|
390
|
-
type="button"
|
|
391
|
-
role="combobox"
|
|
392
|
-
aria-expanded={isOpen}
|
|
393
|
-
aria-haspopup="listbox"
|
|
394
|
-
aria-labelledby="select-label"
|
|
395
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
396
|
-
>
|
|
397
|
-
{selectedOption?.label || 'Select...'}
|
|
398
|
-
</button>
|
|
399
|
-
{isOpen && (
|
|
400
|
-
<ul
|
|
401
|
-
ref={listRef}
|
|
402
|
-
role="listbox"
|
|
403
|
-
aria-labelledby="select-label"
|
|
404
|
-
tabIndex={-1}
|
|
405
|
-
>
|
|
406
|
-
{options.map((option, index) => (
|
|
407
|
-
<li
|
|
408
|
-
key={option.value}
|
|
409
|
-
role="option"
|
|
410
|
-
aria-selected={option.value === value}
|
|
411
|
-
className={index === focusIndex ? 'focused' : ''}
|
|
412
|
-
onClick={() => {
|
|
413
|
-
onChange(option.value);
|
|
414
|
-
setIsOpen(false);
|
|
415
|
-
buttonRef.current?.focus();
|
|
416
|
-
}}
|
|
417
|
-
>
|
|
418
|
-
{option.label}
|
|
419
|
-
</li>
|
|
420
|
-
))}
|
|
421
|
-
</ul>
|
|
422
|
-
)}
|
|
423
|
-
</div>
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
---
|
|
429
|
-
|
|
430
|
-
## Color and Contrast Patterns
|
|
431
|
-
|
|
432
|
-
### Sufficient Contrast
|
|
433
|
-
|
|
434
|
-
```css
|
|
435
|
-
/* CORRECT: AA compliant contrast ratios */
|
|
436
|
-
:root {
|
|
437
|
-
/* Text colors with sufficient contrast on white */
|
|
438
|
-
--text-primary: #1a1a1a; /* 16.1:1 */
|
|
439
|
-
--text-secondary: #595959; /* 7:1 */
|
|
440
|
-
--text-muted: #767676; /* 4.54:1 - minimum for normal text */
|
|
441
|
-
|
|
442
|
-
/* Interactive element colors */
|
|
443
|
-
--link-color: #0066cc; /* 5.9:1 */
|
|
444
|
-
--error-color: #c41e3a; /* 5.4:1 */
|
|
445
|
-
--success-color: #0a6640; /* 7.2:1 */
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/* CORRECT: Placeholder with sufficient contrast */
|
|
449
|
-
input::placeholder {
|
|
450
|
-
color: #767676; /* 4.5:1 minimum */
|
|
451
|
-
}
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
### Non-Color Indicators
|
|
455
|
-
|
|
456
|
-
```tsx
|
|
457
|
-
// CORRECT: Multiple indicators for state
|
|
458
|
-
function StatusBadge({ status }) {
|
|
459
|
-
const config = {
|
|
460
|
-
success: { color: 'green', icon: '✓', label: 'Success' },
|
|
461
|
-
error: { color: 'red', icon: '✕', label: 'Error' },
|
|
462
|
-
warning: { color: 'orange', icon: '⚠', label: 'Warning' },
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
const { color, icon, label } = config[status];
|
|
466
|
-
|
|
467
|
-
return (
|
|
468
|
-
<span className={`badge badge-${color}`}>
|
|
469
|
-
<span aria-hidden="true">{icon}</span>
|
|
470
|
-
{label}
|
|
471
|
-
</span>
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// CORRECT: Chart with patterns
|
|
476
|
-
function AccessibleChart({ data }) {
|
|
477
|
-
const patterns = ['solid', 'dashed', 'dotted', 'dash-dot'];
|
|
478
|
-
|
|
479
|
-
return (
|
|
480
|
-
<LineChart>
|
|
481
|
-
{data.map((series, i) => (
|
|
482
|
-
<Line
|
|
483
|
-
key={series.name}
|
|
484
|
-
stroke={series.color}
|
|
485
|
-
strokeDasharray={patterns[i]}
|
|
486
|
-
name={series.name}
|
|
487
|
-
/>
|
|
488
|
-
))}
|
|
489
|
-
<Legend />
|
|
490
|
-
</LineChart>
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
---
|
|
496
|
-
|
|
497
|
-
## Motion Patterns
|
|
498
|
-
|
|
499
|
-
### Reduced Motion Support
|
|
500
|
-
|
|
501
|
-
```tsx
|
|
502
|
-
// CORRECT: JavaScript reduced motion check
|
|
503
|
-
function AnimatedComponent({ children }) {
|
|
504
|
-
const prefersReduced = useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
505
|
-
|
|
506
|
-
return (
|
|
507
|
-
<motion.div
|
|
508
|
-
initial={{ opacity: 0, y: prefersReduced ? 0 : 20 }}
|
|
509
|
-
animate={{ opacity: 1, y: 0 }}
|
|
510
|
-
transition={{
|
|
511
|
-
duration: prefersReduced ? 0 : 0.3,
|
|
512
|
-
}}
|
|
513
|
-
>
|
|
514
|
-
{children}
|
|
515
|
-
</motion.div>
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
```css
|
|
521
|
-
/* CORRECT: CSS reduced motion */
|
|
522
|
-
.animated-element {
|
|
523
|
-
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
@media (prefers-reduced-motion: reduce) {
|
|
527
|
-
.animated-element {
|
|
528
|
-
transition: none;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/* Or provide minimal transition */
|
|
532
|
-
.animated-element {
|
|
533
|
-
transition: opacity 0.1s ease;
|
|
534
|
-
transform: none !important;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
### Pausable Animations
|
|
540
|
-
|
|
541
|
-
```tsx
|
|
542
|
-
// CORRECT: Carousel with pause controls
|
|
543
|
-
function Carousel({ slides, autoPlay = true }) {
|
|
544
|
-
const [isPaused, setIsPaused] = useState(!autoPlay);
|
|
545
|
-
const [current, setCurrent] = useState(0);
|
|
546
|
-
|
|
547
|
-
useEffect(() => {
|
|
548
|
-
if (isPaused) return;
|
|
549
|
-
|
|
550
|
-
const timer = setInterval(() => {
|
|
551
|
-
setCurrent((c) => (c + 1) % slides.length);
|
|
552
|
-
}, 5000);
|
|
553
|
-
|
|
554
|
-
return () => clearInterval(timer);
|
|
555
|
-
}, [isPaused, slides.length]);
|
|
556
|
-
|
|
557
|
-
return (
|
|
558
|
-
<div
|
|
559
|
-
role="region"
|
|
560
|
-
aria-roledescription="carousel"
|
|
561
|
-
aria-label="Featured content"
|
|
562
|
-
onMouseEnter={() => setIsPaused(true)}
|
|
563
|
-
onMouseLeave={() => setIsPaused(false)}
|
|
564
|
-
onFocus={() => setIsPaused(true)}
|
|
565
|
-
onBlur={() => setIsPaused(false)}
|
|
566
|
-
>
|
|
567
|
-
<button
|
|
568
|
-
aria-label={isPaused ? 'Play carousel' : 'Pause carousel'}
|
|
569
|
-
onClick={() => setIsPaused(!isPaused)}
|
|
570
|
-
>
|
|
571
|
-
{isPaused ? '▶' : '⏸'}
|
|
572
|
-
</button>
|
|
573
|
-
{/* Carousel content */}
|
|
574
|
-
</div>
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
---
|
|
580
|
-
|
|
581
|
-
## Screen Reader Patterns
|
|
582
|
-
|
|
583
|
-
### Visually Hidden Content
|
|
584
|
-
|
|
585
|
-
```css
|
|
586
|
-
/* CORRECT: Screen reader only class */
|
|
587
|
-
.sr-only {
|
|
588
|
-
position: absolute;
|
|
589
|
-
width: 1px;
|
|
590
|
-
height: 1px;
|
|
591
|
-
padding: 0;
|
|
592
|
-
margin: -1px;
|
|
593
|
-
overflow: hidden;
|
|
594
|
-
clip: rect(0, 0, 0, 0);
|
|
595
|
-
white-space: nowrap;
|
|
596
|
-
border: 0;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/* Show on focus for skip links */
|
|
600
|
-
.sr-only-focusable:focus {
|
|
601
|
-
position: static;
|
|
602
|
-
width: auto;
|
|
603
|
-
height: auto;
|
|
604
|
-
padding: inherit;
|
|
605
|
-
margin: inherit;
|
|
606
|
-
overflow: visible;
|
|
607
|
-
clip: auto;
|
|
608
|
-
white-space: normal;
|
|
609
|
-
}
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### Meaningful Link Text
|
|
613
|
-
|
|
614
|
-
```tsx
|
|
615
|
-
// CORRECT: Descriptive link text
|
|
616
|
-
<p>
|
|
617
|
-
Learn about our <a href="/services">premium support services</a>.
|
|
618
|
-
</p>
|
|
619
|
-
|
|
620
|
-
// CORRECT: Context for repeated links
|
|
621
|
-
{posts.map(post => (
|
|
622
|
-
<article>
|
|
623
|
-
<h2>{post.title}</h2>
|
|
624
|
-
<p>{post.excerpt}</p>
|
|
625
|
-
<a href={post.url}>
|
|
626
|
-
Read full article: {post.title}
|
|
627
|
-
<span className="sr-only">, posted {post.date}</span>
|
|
628
|
-
</a>
|
|
629
|
-
</article>
|
|
630
|
-
))}
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
### Proper Document Structure
|
|
634
|
-
|
|
635
|
-
```tsx
|
|
636
|
-
// CORRECT: Landmark regions and heading hierarchy
|
|
637
|
-
function App() {
|
|
638
|
-
return (
|
|
639
|
-
<>
|
|
640
|
-
<a href="#main" className="sr-only-focusable">
|
|
641
|
-
Skip to main content
|
|
642
|
-
</a>
|
|
643
|
-
|
|
644
|
-
<header>
|
|
645
|
-
<nav aria-label="Main">
|
|
646
|
-
{/* Navigation */}
|
|
647
|
-
</nav>
|
|
648
|
-
</header>
|
|
649
|
-
|
|
650
|
-
<aside aria-label="Sidebar">
|
|
651
|
-
{/* Sidebar content */}
|
|
652
|
-
</aside>
|
|
653
|
-
|
|
654
|
-
<main id="main" tabIndex={-1}>
|
|
655
|
-
<h1>Page Title</h1>
|
|
656
|
-
<section aria-labelledby="section-1">
|
|
657
|
-
<h2 id="section-1">First Section</h2>
|
|
658
|
-
<h3>Subsection</h3>
|
|
659
|
-
</section>
|
|
660
|
-
</main>
|
|
661
|
-
|
|
662
|
-
<footer>
|
|
663
|
-
<nav aria-label="Footer">
|
|
664
|
-
{/* Footer navigation */}
|
|
665
|
-
</nav>
|
|
666
|
-
</footer>
|
|
667
|
-
</>
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
```
|