@a11y-context/mcp-server 0.1.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/LICENSE +201 -0
- package/README.md +62 -0
- package/corpus/web/react/components/accordion.basic.md +157 -0
- package/corpus/web/react/components/button.basic.md +91 -0
- package/corpus/web/react/components/button.toggle.md +109 -0
- package/corpus/web/react/components/carousel.dots.md +376 -0
- package/corpus/web/react/components/carousel.thumbnails.md +395 -0
- package/corpus/web/react/components/collection-row.basic.md +179 -0
- package/corpus/web/react/components/combobox.autocomplete.md +293 -0
- package/corpus/web/react/components/dialog.modal.md +202 -0
- package/corpus/web/react/components/dialog.nonmodal.md +184 -0
- package/corpus/web/react/components/disclosure.basic.md +97 -0
- package/corpus/web/react/components/grid.channel-guide.md +453 -0
- package/corpus/web/react/components/link.basic.md +105 -0
- package/corpus/web/react/components/listbox.basic.md +263 -0
- package/corpus/web/react/components/menu.basic.md +294 -0
- package/corpus/web/react/components/menu.menubar.md +296 -0
- package/corpus/web/react/components/navigation-menu.basic.md +349 -0
- package/corpus/web/react/components/navigation-menu.dropdown.md +220 -0
- package/corpus/web/react/components/select.basic.md +318 -0
- package/corpus/web/react/components/select.native.md +108 -0
- package/corpus/web/react/components/switch.basic.md +151 -0
- package/corpus/web/react/components/toast.basic.md +112 -0
- package/corpus/web/react/components/tooltip.basic.md +139 -0
- package/corpus/web/react/global/global_rules.md +292 -0
- package/corpus/web/react/patterns.json +946 -0
- package/dist/config.js +35 -0
- package/dist/contracts/v1/types.js +6 -0
- package/dist/http.js +103 -0
- package/dist/index.js +8 -0
- package/dist/mcp/createServer.js +122 -0
- package/dist/mcp/response.js +7 -0
- package/dist/mcp/server.js +15 -0
- package/dist/repo/cache.js +27 -0
- package/dist/repo/globalRules.js +304 -0
- package/dist/repo/index.js +178 -0
- package/dist/repo/paths.js +25 -0
- package/dist/repo/sections.js +166 -0
- package/dist/smoke/smoke-remote-mcp.js +43 -0
- package/dist/telemetry.js +15 -0
- package/dist/telemetryWrap.js +48 -0
- package/dist/tools/getGlobalRules.js +33 -0
- package/dist/tools/getPattern.js +54 -0
- package/dist/tools/listPatterns.js +33 -0
- package/dist/utils/fs.js +18 -0
- package/dist/utils/hash.js +4 -0
- package/package.json +68 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: carousel.dots
|
|
3
|
+
title: Carousel with Dot Navigation
|
|
4
|
+
stack: web/react
|
|
5
|
+
status: beta
|
|
6
|
+
latest_version: 0.2.0
|
|
7
|
+
tags: [marquee, carousel, slider, gallery, dots, autoplay, reduced-motion]
|
|
8
|
+
aliases: [hero carousel, marquee, featured gallery, hero gallery, image gallery, slider]
|
|
9
|
+
summary: Horizontally-advancing carousel (aka hero or marquee carousel) with 'dot' navigation, prev/next buttons, and pause behavior.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Carousel with Dot Navigation
|
|
13
|
+
|
|
14
|
+
Pattern ID: `carousel.dots`
|
|
15
|
+
|
|
16
|
+
Horizontally-advancing carousel (aka hero or marquee carousel) with dot navigation, prev/next buttons, and pause behavior.
|
|
17
|
+
|
|
18
|
+
## Use When
|
|
19
|
+
- Use when slides are navigated sequentially using next/previous controls or dot indicators.
|
|
20
|
+
- Use when dot buttons indicate slide position but do not display image previews.
|
|
21
|
+
|
|
22
|
+
## Do Not Use When
|
|
23
|
+
- Do not use when thumbnail images are shown for direct slide selection (use `carousel.thumbnails`).
|
|
24
|
+
- Do not use when multiple slides are visible at once in a horizontal list (use `content-shelf`).
|
|
25
|
+
|
|
26
|
+
## Must Haves
|
|
27
|
+
- Render a carousel container with `aria-roledescription="carousel"` and an accessible name (`aria-label`).
|
|
28
|
+
- Ensure the carousel container has a semantic HTML5 element or role, such as `<section>` or `role="region"`.
|
|
29
|
+
- Each slide must have `role="group"`, `aria-roledescription="slide"` and an `aria-label` like "1 of N" (N is total number of slides).
|
|
30
|
+
- Slides that are not currently visible must not be rendered, or must be hidden in the DOM (e.g., via the `hidden` attribute), so their content cannot be reached by keyboard or screen readers.
|
|
31
|
+
- Provide Previous/Next buttons as real `<button>` elements, with `aria-label` like "Previous Slide" and "Next Slide".
|
|
32
|
+
- Provide dot navigation as real `<button>` elements in normal tab order (no roving tabindex), with `aria-label` like "Go to slide 2", and with `aria-current="true"` on the button corresponding to the active slide.
|
|
33
|
+
- Provide a Pause/Play button as the first focusable element inside the carousel container.
|
|
34
|
+
- Default to paused when `prefers-reduced-motion: reduce`.
|
|
35
|
+
- Pause when keyboard focus enters the carousel region.
|
|
36
|
+
- Ensure a visible focus state (e.g., a 2px solid outline offset by 1-2px) on each focusable element, including the previous/next buttons, pause button, and dots.
|
|
37
|
+
|
|
38
|
+
## Customizable
|
|
39
|
+
- The contents of each slide are customizable. However, if they contain a title, then these should usually be `<h2>`.
|
|
40
|
+
|
|
41
|
+
## Don'ts
|
|
42
|
+
- Do not auto-advance the slides without a visible Pause/Play control.
|
|
43
|
+
- Do not ignore `prefers-reduced-motion`.
|
|
44
|
+
- Do not keep moving while the user is interacting (focus inside carousel must pause autoplay).
|
|
45
|
+
|
|
46
|
+
## Golden Pattern
|
|
47
|
+
|
|
48
|
+
Structural reference for AI coding assistants — semantics, focus, and keyboard behavior. Styling, copy, and demo data are illustrative.
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
"use client";
|
|
52
|
+
|
|
53
|
+
export function CarouselDotsDemo({
|
|
54
|
+
ariaLabel = "Featured content",
|
|
55
|
+
items = DEFAULT_ITEMS,
|
|
56
|
+
autoplay = true,
|
|
57
|
+
intervalMs = 5000,
|
|
58
|
+
}) {
|
|
59
|
+
const [index, setIndex] = useState(0);
|
|
60
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
61
|
+
|
|
62
|
+
const hasUserToggledPauseRef = useRef(false);
|
|
63
|
+
const reducedMotionRef = useRef(false);
|
|
64
|
+
const timerRef = useRef(null);
|
|
65
|
+
const skipFocusPauseRef = useRef(false);
|
|
66
|
+
|
|
67
|
+
const count = items.length;
|
|
68
|
+
|
|
69
|
+
// Reduced motion: paused by default.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
72
|
+
const apply = () => {
|
|
73
|
+
reducedMotionRef.current = !!mq.matches;
|
|
74
|
+
if (mq.matches) {
|
|
75
|
+
setIsPaused(true);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
apply();
|
|
80
|
+
|
|
81
|
+
// Safari still supports addListener in some versions
|
|
82
|
+
if (mq.addEventListener) mq.addEventListener("change", apply);
|
|
83
|
+
else mq.addListener(apply);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
if (mq.removeEventListener) mq.removeEventListener("change", apply);
|
|
87
|
+
else mq.removeListener(apply);
|
|
88
|
+
};
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
function goTo(nextIndex) {
|
|
92
|
+
const clamped = ((nextIndex % count) + count) % count;
|
|
93
|
+
setIndex(clamped);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function goPrev() {
|
|
97
|
+
goTo(index - 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function goNext() {
|
|
101
|
+
goTo(index + 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pause() {
|
|
105
|
+
setIsPaused(true);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function togglePause() {
|
|
109
|
+
skipFocusPauseRef.current = false;
|
|
110
|
+
hasUserToggledPauseRef.current = true;
|
|
111
|
+
setIsPaused((p) => !p);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onPauseButtonPointerDown() {
|
|
115
|
+
skipFocusPauseRef.current = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Pause autoplay when focus enters the carousel.
|
|
119
|
+
function onFocusCapture() {
|
|
120
|
+
if (skipFocusPauseRef.current) {
|
|
121
|
+
skipFocusPauseRef.current = false;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
pause();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Autoplay (respects reduced motion, focus-paused state, and user pause).
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!autoplay || isPaused || reducedMotionRef.current) return;
|
|
130
|
+
|
|
131
|
+
// Functional update avoids stale-closure reads of the current index.
|
|
132
|
+
timerRef.current = window.setInterval(() => {
|
|
133
|
+
setIndex((i) => (i + 1) % count);
|
|
134
|
+
}, intervalMs);
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
if (timerRef.current) {
|
|
138
|
+
window.clearInterval(timerRef.current);
|
|
139
|
+
timerRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}, [autoplay, isPaused, intervalMs, count]);
|
|
143
|
+
|
|
144
|
+
const active = items[index];
|
|
145
|
+
|
|
146
|
+
// aria-live: off while moving, polite when paused so changes can be announced if user moves slides.
|
|
147
|
+
const ariaLive = isPaused ? "polite" : "off";
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<section
|
|
151
|
+
aria-roledescription="carousel"
|
|
152
|
+
aria-label={ariaLabel}
|
|
153
|
+
onFocusCapture={onFocusCapture}
|
|
154
|
+
style={{
|
|
155
|
+
position: "relative",
|
|
156
|
+
maxWidth: 960,
|
|
157
|
+
margin: "0 auto",
|
|
158
|
+
padding: 16,
|
|
159
|
+
background: "#fff",
|
|
160
|
+
color: "#111",
|
|
161
|
+
borderRadius: 12,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{/* Slides viewport */}
|
|
165
|
+
<div
|
|
166
|
+
aria-live={ariaLive}
|
|
167
|
+
style={{
|
|
168
|
+
position: "relative",
|
|
169
|
+
overflow: "hidden",
|
|
170
|
+
borderRadius: 12,
|
|
171
|
+
minHeight: 400,
|
|
172
|
+
background: "#000",
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onPointerDown={onPauseButtonPointerDown}
|
|
178
|
+
onClick={togglePause}
|
|
179
|
+
aria-label={isPaused ? "Play automatic rotation" : "Pause automatic rotation"}
|
|
180
|
+
aria-pressed={isPaused}
|
|
181
|
+
style={pauseButtonStyle}
|
|
182
|
+
>
|
|
183
|
+
<span aria-hidden="true">
|
|
184
|
+
{isPaused ? "[play-icon]" : "[pause-icon]"}
|
|
185
|
+
</span>
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
{/* Slide */}
|
|
189
|
+
<div
|
|
190
|
+
aria-roledescription="slide"
|
|
191
|
+
aria-label={`${index + 1} of ${count}`}
|
|
192
|
+
style={{
|
|
193
|
+
display: "grid",
|
|
194
|
+
gridTemplateColumns: "1fr",
|
|
195
|
+
alignItems: "end",
|
|
196
|
+
minHeight: 400,
|
|
197
|
+
padding: "16px 72px 40px",
|
|
198
|
+
backgroundImage: active.image ? `url(${active.image})` : undefined,
|
|
199
|
+
backgroundSize: "cover",
|
|
200
|
+
backgroundPosition: "center",
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
style={{
|
|
205
|
+
maxWidth: 520,
|
|
206
|
+
background: "#000",
|
|
207
|
+
color: "#fff",
|
|
208
|
+
padding: 12,
|
|
209
|
+
borderRadius: 10,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<h2 style={{ margin: 0, fontSize: 24 }}>{active.title}</h2>
|
|
213
|
+
<p style={{ marginTop: 8, marginBottom: 12, lineHeight: 1.4 }}>
|
|
214
|
+
{active.description}
|
|
215
|
+
</p>
|
|
216
|
+
<a
|
|
217
|
+
href={active.href}
|
|
218
|
+
style={{
|
|
219
|
+
display: "inline-block",
|
|
220
|
+
padding: "10px 12px",
|
|
221
|
+
borderRadius: 10,
|
|
222
|
+
background: "#fff",
|
|
223
|
+
color: "#000",
|
|
224
|
+
textDecoration: "none",
|
|
225
|
+
fontWeight: 600,
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
View details
|
|
229
|
+
</a>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Prev / Next (inside visible slide area) */}
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={() => {
|
|
237
|
+
pause();
|
|
238
|
+
goPrev();
|
|
239
|
+
}}
|
|
240
|
+
aria-label="Previous slide"
|
|
241
|
+
style={navButtonStyle("left")}
|
|
242
|
+
>
|
|
243
|
+
‹
|
|
244
|
+
</button>
|
|
245
|
+
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={() => {
|
|
249
|
+
pause();
|
|
250
|
+
goNext();
|
|
251
|
+
}}
|
|
252
|
+
aria-label="Next slide"
|
|
253
|
+
style={navButtonStyle("right")}
|
|
254
|
+
>
|
|
255
|
+
›
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Controls row below the viewport */}
|
|
260
|
+
<div
|
|
261
|
+
style={{
|
|
262
|
+
display: "flex",
|
|
263
|
+
alignItems: "center",
|
|
264
|
+
justifyContent: "center",
|
|
265
|
+
marginTop: 12,
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
<div style={{ display: "flex", gap: 8 }} aria-label="Choose a slide">
|
|
269
|
+
{items.map((_, i) => {
|
|
270
|
+
const isActive = i === index;
|
|
271
|
+
return (
|
|
272
|
+
<button
|
|
273
|
+
key={i}
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={() => {
|
|
276
|
+
pause();
|
|
277
|
+
goTo(i);
|
|
278
|
+
}}
|
|
279
|
+
aria-label={`Go to slide ${i + 1}`}
|
|
280
|
+
aria-current={isActive ? "true" : undefined}
|
|
281
|
+
style={dotStyle(isActive)}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</section>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function navButtonStyle(side) {
|
|
292
|
+
return {
|
|
293
|
+
position: "absolute",
|
|
294
|
+
top: "50%",
|
|
295
|
+
transform: "translateY(-50%)",
|
|
296
|
+
[side]: 12,
|
|
297
|
+
width: 44,
|
|
298
|
+
height: 44,
|
|
299
|
+
borderRadius: 12,
|
|
300
|
+
border: "1px solid rgba(255,255,255,0.25)",
|
|
301
|
+
background: "rgba(0,0,0,0.45)",
|
|
302
|
+
color: "#fff",
|
|
303
|
+
display: "grid",
|
|
304
|
+
placeItems: "center",
|
|
305
|
+
cursor: "pointer",
|
|
306
|
+
fontSize: 28,
|
|
307
|
+
lineHeight: 1,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const pauseButtonStyle = {
|
|
312
|
+
position: "absolute",
|
|
313
|
+
top: 12,
|
|
314
|
+
left: 12,
|
|
315
|
+
zIndex: 2,
|
|
316
|
+
width: 40,
|
|
317
|
+
height: 40,
|
|
318
|
+
borderRadius: 10,
|
|
319
|
+
border: "1px solid rgba(255,255,255,0.25)",
|
|
320
|
+
background: "rgba(0,0,0,0.6)",
|
|
321
|
+
color: "#fff",
|
|
322
|
+
display: "grid",
|
|
323
|
+
placeItems: "center",
|
|
324
|
+
cursor: "pointer",
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
function dotStyle(active) {
|
|
328
|
+
return {
|
|
329
|
+
width: 12,
|
|
330
|
+
height: 12,
|
|
331
|
+
borderRadius: 999,
|
|
332
|
+
border: "1px solid rgba(0,0,0,0.35)",
|
|
333
|
+
background: active ? "#111" : "rgba(0,0,0,0.18)",
|
|
334
|
+
cursor: "pointer",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const DEFAULT_ITEMS = [
|
|
339
|
+
{
|
|
340
|
+
title: "Neighbors",
|
|
341
|
+
description: "A chaotic dispute spirals. Watch the latest episode now.",
|
|
342
|
+
href: "#",
|
|
343
|
+
image: "https://picsum.photos/seed/hero-1/1200/600",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
title: "UCLA at Michigan",
|
|
347
|
+
description: "Tip-off at 12:45 PM ET. Catch it live.",
|
|
348
|
+
href: "#",
|
|
349
|
+
image: "https://picsum.photos/seed/hero-2/1200/600",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
title: "Fire Country",
|
|
353
|
+
description: "A risky mission tests loyalties and nerves.",
|
|
354
|
+
href: "#",
|
|
355
|
+
image: "https://picsum.photos/seed/hero-3/1200/600",
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Acceptance Checks
|
|
361
|
+
- Semantics:
|
|
362
|
+
- The carousel container has `aria-roledescription="carousel"` and an accessible name.
|
|
363
|
+
- Each slide has `aria-roledescription="slide"` and exposes position (e.g., "2 of 3").
|
|
364
|
+
- Content of non-visible slides is not reachable by keyboard or screen reader.
|
|
365
|
+
- Autoplay:
|
|
366
|
+
- With `prefers-reduced-motion: reduce`, autoplay is paused by default.
|
|
367
|
+
- Tabbing into the carousel pauses autoplay.
|
|
368
|
+
- Autoplay does not run while paused.
|
|
369
|
+
- Controls (keyboard):
|
|
370
|
+
- Previous and Next buttons are reachable via Tab and move one slide per activation.
|
|
371
|
+
- Each dot is reachable via Tab and activates its corresponding slide.
|
|
372
|
+
- The active dot exposes state (e.g., `aria-current="true"`).
|
|
373
|
+
- Content:
|
|
374
|
+
- Each slide includes a visible title (`<h2>`), a short description, and one primary CTA link.
|
|
375
|
+
- Screen reader:
|
|
376
|
+
- When changing slides while paused, the carousel name and slide position are announced without duplicate announcements.
|