devflow-kit 1.1.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +23 -6
  3. package/dist/plugins.js +67 -3
  4. package/package.json +2 -1
  5. package/plugins/devflow-accessibility/.claude-plugin/plugin.json +15 -0
  6. package/plugins/devflow-ambient/.claude-plugin/plugin.json +1 -1
  7. package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +1 -1
  8. package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +4 -0
  9. package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
  10. package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -4
  11. package/plugins/devflow-code-review/agents/reviewer.md +8 -0
  12. package/plugins/devflow-code-review/commands/code-review-teams.md +11 -1
  13. package/plugins/devflow-code-review/commands/code-review.md +12 -2
  14. package/plugins/devflow-core-skills/.claude-plugin/plugin.json +2 -6
  15. package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
  16. package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +15 -0
  17. package/plugins/devflow-go/.claude-plugin/plugin.json +15 -0
  18. package/plugins/devflow-go/skills/go/SKILL.md +187 -0
  19. package/plugins/devflow-go/skills/go/references/concurrency.md +312 -0
  20. package/plugins/devflow-go/skills/go/references/detection.md +129 -0
  21. package/plugins/devflow-go/skills/go/references/patterns.md +232 -0
  22. package/plugins/devflow-go/skills/go/references/violations.md +205 -0
  23. package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -3
  24. package/plugins/devflow-implement/agents/coder.md +11 -6
  25. package/plugins/devflow-java/.claude-plugin/plugin.json +15 -0
  26. package/plugins/devflow-java/skills/java/SKILL.md +183 -0
  27. package/plugins/devflow-java/skills/java/references/detection.md +120 -0
  28. package/plugins/devflow-java/skills/java/references/modern-java.md +270 -0
  29. package/plugins/devflow-java/skills/java/references/patterns.md +235 -0
  30. package/plugins/devflow-java/skills/java/references/violations.md +213 -0
  31. package/plugins/devflow-python/.claude-plugin/plugin.json +15 -0
  32. package/plugins/devflow-python/skills/python/SKILL.md +188 -0
  33. package/plugins/devflow-python/skills/python/references/async.md +220 -0
  34. package/plugins/devflow-python/skills/python/references/detection.md +128 -0
  35. package/plugins/devflow-python/skills/python/references/patterns.md +226 -0
  36. package/plugins/devflow-python/skills/python/references/violations.md +204 -0
  37. package/plugins/devflow-react/.claude-plugin/plugin.json +15 -0
  38. package/plugins/{devflow-core-skills → devflow-react}/skills/react/SKILL.md +1 -1
  39. package/plugins/{devflow-core-skills → devflow-react}/skills/react/references/patterns.md +3 -3
  40. package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
  41. package/plugins/devflow-rust/.claude-plugin/plugin.json +15 -0
  42. package/plugins/devflow-rust/skills/rust/SKILL.md +193 -0
  43. package/plugins/devflow-rust/skills/rust/references/detection.md +131 -0
  44. package/plugins/devflow-rust/skills/rust/references/ownership.md +242 -0
  45. package/plugins/devflow-rust/skills/rust/references/patterns.md +210 -0
  46. package/plugins/devflow-rust/skills/rust/references/violations.md +191 -0
  47. package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
  48. package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
  49. package/plugins/devflow-typescript/.claude-plugin/plugin.json +15 -0
  50. package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/patterns.md +3 -3
  51. package/shared/agents/coder.md +11 -6
  52. package/shared/agents/reviewer.md +8 -0
  53. package/shared/skills/ambient-router/SKILL.md +1 -1
  54. package/shared/skills/ambient-router/references/skill-catalog.md +4 -0
  55. package/shared/skills/go/SKILL.md +187 -0
  56. package/shared/skills/go/references/concurrency.md +312 -0
  57. package/shared/skills/go/references/detection.md +129 -0
  58. package/shared/skills/go/references/patterns.md +232 -0
  59. package/shared/skills/go/references/violations.md +205 -0
  60. package/shared/skills/java/SKILL.md +183 -0
  61. package/shared/skills/java/references/detection.md +120 -0
  62. package/shared/skills/java/references/modern-java.md +270 -0
  63. package/shared/skills/java/references/patterns.md +235 -0
  64. package/shared/skills/java/references/violations.md +213 -0
  65. package/shared/skills/python/SKILL.md +188 -0
  66. package/shared/skills/python/references/async.md +220 -0
  67. package/shared/skills/python/references/detection.md +128 -0
  68. package/shared/skills/python/references/patterns.md +226 -0
  69. package/shared/skills/python/references/violations.md +204 -0
  70. package/shared/skills/react/SKILL.md +1 -1
  71. package/shared/skills/react/references/patterns.md +3 -3
  72. package/shared/skills/rust/SKILL.md +193 -0
  73. package/shared/skills/rust/references/detection.md +131 -0
  74. package/shared/skills/rust/references/ownership.md +242 -0
  75. package/shared/skills/rust/references/patterns.md +210 -0
  76. package/shared/skills/rust/references/violations.md +191 -0
  77. package/shared/skills/typescript/references/patterns.md +3 -3
  78. package/plugins/devflow-code-review/skills/react/SKILL.md +0 -276
  79. package/plugins/devflow-code-review/skills/react/references/patterns.md +0 -1331
  80. package/plugins/devflow-core-skills/skills/accessibility/SKILL.md +0 -229
  81. package/plugins/devflow-core-skills/skills/accessibility/references/detection.md +0 -171
  82. package/plugins/devflow-core-skills/skills/accessibility/references/patterns.md +0 -670
  83. package/plugins/devflow-core-skills/skills/accessibility/references/violations.md +0 -419
  84. package/plugins/devflow-core-skills/skills/frontend-design/SKILL.md +0 -254
  85. package/plugins/devflow-core-skills/skills/frontend-design/references/detection.md +0 -184
  86. package/plugins/devflow-core-skills/skills/frontend-design/references/patterns.md +0 -511
  87. package/plugins/devflow-core-skills/skills/frontend-design/references/violations.md +0 -453
  88. package/plugins/devflow-core-skills/skills/react/references/violations.md +0 -565
  89. package/plugins/devflow-implement/skills/accessibility/SKILL.md +0 -229
  90. package/plugins/devflow-implement/skills/accessibility/references/detection.md +0 -171
  91. package/plugins/devflow-implement/skills/accessibility/references/patterns.md +0 -670
  92. package/plugins/devflow-implement/skills/accessibility/references/violations.md +0 -419
  93. package/plugins/devflow-implement/skills/frontend-design/SKILL.md +0 -254
  94. package/plugins/devflow-implement/skills/frontend-design/references/detection.md +0 -184
  95. package/plugins/devflow-implement/skills/frontend-design/references/patterns.md +0 -511
  96. package/plugins/devflow-implement/skills/frontend-design/references/violations.md +0 -453
  97. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/SKILL.md +0 -0
  98. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/detection.md +0 -0
  99. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/patterns.md +0 -0
  100. /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/violations.md +0 -0
  101. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/SKILL.md +0 -0
  102. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/detection.md +0 -0
  103. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/patterns.md +0 -0
  104. /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/violations.md +0 -0
  105. /package/plugins/{devflow-code-review → devflow-react}/skills/react/references/violations.md +0 -0
  106. /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/SKILL.md +0 -0
  107. /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
- ```