@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.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +62 -0
  3. package/corpus/web/react/components/accordion.basic.md +157 -0
  4. package/corpus/web/react/components/button.basic.md +91 -0
  5. package/corpus/web/react/components/button.toggle.md +109 -0
  6. package/corpus/web/react/components/carousel.dots.md +376 -0
  7. package/corpus/web/react/components/carousel.thumbnails.md +395 -0
  8. package/corpus/web/react/components/collection-row.basic.md +179 -0
  9. package/corpus/web/react/components/combobox.autocomplete.md +293 -0
  10. package/corpus/web/react/components/dialog.modal.md +202 -0
  11. package/corpus/web/react/components/dialog.nonmodal.md +184 -0
  12. package/corpus/web/react/components/disclosure.basic.md +97 -0
  13. package/corpus/web/react/components/grid.channel-guide.md +453 -0
  14. package/corpus/web/react/components/link.basic.md +105 -0
  15. package/corpus/web/react/components/listbox.basic.md +263 -0
  16. package/corpus/web/react/components/menu.basic.md +294 -0
  17. package/corpus/web/react/components/menu.menubar.md +296 -0
  18. package/corpus/web/react/components/navigation-menu.basic.md +349 -0
  19. package/corpus/web/react/components/navigation-menu.dropdown.md +220 -0
  20. package/corpus/web/react/components/select.basic.md +318 -0
  21. package/corpus/web/react/components/select.native.md +108 -0
  22. package/corpus/web/react/components/switch.basic.md +151 -0
  23. package/corpus/web/react/components/toast.basic.md +112 -0
  24. package/corpus/web/react/components/tooltip.basic.md +139 -0
  25. package/corpus/web/react/global/global_rules.md +292 -0
  26. package/corpus/web/react/patterns.json +946 -0
  27. package/dist/config.js +35 -0
  28. package/dist/contracts/v1/types.js +6 -0
  29. package/dist/http.js +103 -0
  30. package/dist/index.js +8 -0
  31. package/dist/mcp/createServer.js +122 -0
  32. package/dist/mcp/response.js +7 -0
  33. package/dist/mcp/server.js +15 -0
  34. package/dist/repo/cache.js +27 -0
  35. package/dist/repo/globalRules.js +304 -0
  36. package/dist/repo/index.js +178 -0
  37. package/dist/repo/paths.js +25 -0
  38. package/dist/repo/sections.js +166 -0
  39. package/dist/smoke/smoke-remote-mcp.js +43 -0
  40. package/dist/telemetry.js +15 -0
  41. package/dist/telemetryWrap.js +48 -0
  42. package/dist/tools/getGlobalRules.js +33 -0
  43. package/dist/tools/getPattern.js +54 -0
  44. package/dist/tools/listPatterns.js +33 -0
  45. package/dist/utils/fs.js +18 -0
  46. package/dist/utils/hash.js +4 -0
  47. package/package.json +68 -0
@@ -0,0 +1,395 @@
1
+ ---
2
+ id: carousel.thumbnails
3
+ title: Carousel with Thumbnail Navigation
4
+ stack: web/react
5
+ status: beta
6
+ latest_version: 0.2.0
7
+ tags: [marquee, carousel, slider, gallery, thumbnails, autoplay, reduced-motion, image-preview]
8
+ aliases: [hero carousel thumbnails, marquee thumbnails, featured gallery thumbnails, hero gallery thumbnails, preview carousel]
9
+ summary: Horizontally-advancing carousel (aka hero or marquee carousel) with thumbnail navigation, prev/next buttons, and pause behavior.
10
+ ---
11
+
12
+ # Carousel with Thumbnail Navigation
13
+
14
+ Pattern ID: `carousel.thumbnails`
15
+
16
+ Horizontally-advancing carousel (aka hero or marquee carousel) with thumbnail navigation, prev/next buttons, and pause behavior.
17
+
18
+ ## Use When
19
+ - Use when thumbnail images are displayed for direct slide selection.
20
+ - Use when users can identify slide content from visible preview images before activating a slide.
21
+ - Use when thumbnails act as explicit navigation controls for specific slides.
22
+
23
+ ## Do Not Use When
24
+ - Do not use when navigation is limited to dot indicators without image previews (use `carousel.dots`).
25
+ - Do not use when multiple full-size items are visible simultaneously in a scrollable row (use `content-shelf`).
26
+
27
+ ## Must Haves
28
+ - Render a carousel container with `aria-roledescription="carousel"` and an accessible name (`aria-label`).
29
+ - Ensure the carousel container has a semantic HTML5 element or role, such as `<section>` or `role="region"`.
30
+ - Each slide must have `role="group"`, `aria-roledescription="slide"` and an `aria-label` like "1 of N".
31
+ - 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.
32
+ - Provide Previous/Next buttons as real `<button>` elements, with `aria-label` like "Previous Slide" and "Next Slide".
33
+ - Provide thumbnail navigation as real `<button>` elements in normal tab order (no roving tabindex), with `aria-label` like "Go to slide 2: {title of slide 2}", and with `aria-current="true"` on the button corresponding to the active slide.
34
+ - Provide a Pause/Play button as the first focusable element inside the carousel container.
35
+ - Default to paused when prefers-reduced-motion: reduce.
36
+ - Pause when keyboard focus enters the carousel region.
37
+ - 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 thumbnail controls.
38
+
39
+ ## Customizable
40
+ - The contents of each slide are customizable. However, if they contain a title, then these should usually be `<h2>`.
41
+
42
+ ## Don'ts
43
+ - Do not auto-advance the slides without a visible Pause/Play control.
44
+ - Do not ignore `prefers-reduced-motion: reduce`.
45
+ - Do not keep moving while the user is interacting (focus inside carousel must pause autoplay).
46
+
47
+ ## Golden Pattern
48
+
49
+ Structural reference for AI coding assistants — semantics, focus, and keyboard behavior. Styling, copy, and demo data are illustrative.
50
+
51
+ ```jsx
52
+ "use client";
53
+
54
+ export function CarouselThumbnailsDemo({
55
+ ariaLabel = "Featured content",
56
+ items = DEFAULT_ITEMS,
57
+ autoplay = true,
58
+ intervalMs = 5000,
59
+ }) {
60
+ const [index, setIndex] = useState(0);
61
+ const [isPaused, setIsPaused] = useState(false);
62
+
63
+ const hasUserToggledPauseRef = useRef(false);
64
+ const reducedMotionRef = useRef(false);
65
+ const timerRef = useRef(null);
66
+ const skipFocusPauseRef = useRef(false);
67
+
68
+ const count = items.length;
69
+
70
+ // Reduced motion: paused by default.
71
+ useEffect(() => {
72
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
73
+ const apply = () => {
74
+ reducedMotionRef.current = !!mq.matches;
75
+ if (mq.matches) {
76
+ setIsPaused(true);
77
+ }
78
+ };
79
+
80
+ apply();
81
+
82
+ // Safari still supports addListener in some versions
83
+ if (mq.addEventListener) mq.addEventListener("change", apply);
84
+ else mq.addListener(apply);
85
+
86
+ return () => {
87
+ if (mq.removeEventListener) mq.removeEventListener("change", apply);
88
+ else mq.removeListener(apply);
89
+ };
90
+ }, []);
91
+
92
+ function goTo(nextIndex) {
93
+ const clamped = ((nextIndex % count) + count) % count;
94
+ setIndex(clamped);
95
+ }
96
+
97
+ function goPrev() {
98
+ goTo(index - 1);
99
+ }
100
+
101
+ function goNext() {
102
+ goTo(index + 1);
103
+ }
104
+
105
+ function pause() {
106
+ setIsPaused(true);
107
+ }
108
+
109
+ function togglePause() {
110
+ skipFocusPauseRef.current = false;
111
+ hasUserToggledPauseRef.current = true;
112
+ setIsPaused((p) => !p);
113
+ }
114
+
115
+ function onPauseButtonPointerDown() {
116
+ skipFocusPauseRef.current = true;
117
+ }
118
+
119
+ // Pause autoplay when focus enters the carousel.
120
+ function onFocusCapture() {
121
+ if (skipFocusPauseRef.current) {
122
+ skipFocusPauseRef.current = false;
123
+ return;
124
+ }
125
+ pause();
126
+ }
127
+
128
+ // Autoplay (respects reduced motion, focus-paused state, and user pause).
129
+ useEffect(() => {
130
+ if (!autoplay || isPaused || reducedMotionRef.current) return;
131
+
132
+ // Functional update avoids stale-closure reads of the current index.
133
+ timerRef.current = window.setInterval(() => {
134
+ setIndex((i) => (i + 1) % count);
135
+ }, intervalMs);
136
+
137
+ return () => {
138
+ if (timerRef.current) {
139
+ window.clearInterval(timerRef.current);
140
+ timerRef.current = null;
141
+ }
142
+ };
143
+ }, [autoplay, isPaused, intervalMs, count]);
144
+
145
+ const active = items[index];
146
+
147
+ // aria-live: off while moving, polite when paused so changes can be announced if user moves slides.
148
+ const ariaLive = isPaused ? "polite" : "off";
149
+
150
+ return (
151
+ <section
152
+ aria-roledescription="carousel"
153
+ aria-label={ariaLabel}
154
+ onFocusCapture={onFocusCapture}
155
+ style={{
156
+ position: "relative",
157
+ maxWidth: 960,
158
+ margin: "0 auto",
159
+ padding: 16,
160
+ background: "#fff",
161
+ color: "#111",
162
+ borderRadius: 12,
163
+ }}
164
+ >
165
+ {/* Slides viewport */}
166
+ <div
167
+ aria-live={ariaLive}
168
+ style={{
169
+ position: "relative",
170
+ overflow: "hidden",
171
+ borderRadius: 12,
172
+ minHeight: 400,
173
+ background: "#000",
174
+ }}
175
+ >
176
+ <button
177
+ type="button"
178
+ onPointerDown={onPauseButtonPointerDown}
179
+ onClick={togglePause}
180
+ aria-label={isPaused ? "Play automatic rotation" : "Pause automatic rotation"}
181
+ aria-pressed={isPaused}
182
+ style={pauseButtonStyle}
183
+ >
184
+ <span aria-hidden="true">
185
+ {isPaused ? "[play-icon]" : "[pause-icon]"}
186
+ </span>
187
+ </button>
188
+
189
+ {/* Slide */}
190
+ <div
191
+ aria-roledescription="slide"
192
+ aria-label={`${index + 1} of ${count}`}
193
+ style={{
194
+ display: "grid",
195
+ gridTemplateColumns: "1fr",
196
+ alignItems: "end",
197
+ minHeight: 400,
198
+ padding: "16px 72px 72px",
199
+ backgroundImage: active.image ? `url(${active.image})` : undefined,
200
+ backgroundSize: "cover",
201
+ backgroundPosition: "center",
202
+ }}
203
+ >
204
+ <div
205
+ style={{
206
+ maxWidth: 520,
207
+ background: "#000",
208
+ color: "#fff",
209
+ padding: 12,
210
+ borderRadius: 10,
211
+ }}
212
+ >
213
+ <h2 style={{ margin: 0, fontSize: 24 }}>{active.title}</h2>
214
+ <p style={{ marginTop: 8, marginBottom: 12, lineHeight: 1.4 }}>
215
+ {active.description}
216
+ </p>
217
+ <a
218
+ href={active.href}
219
+ style={{
220
+ display: "inline-block",
221
+ padding: "10px 12px",
222
+ borderRadius: 10,
223
+ background: "#fff",
224
+ color: "#000",
225
+ textDecoration: "none",
226
+ fontWeight: 600,
227
+ }}
228
+ >
229
+ View details
230
+ </a>
231
+ </div>
232
+ </div>
233
+
234
+ {/* Prev / Next (inside visible slide area) */}
235
+ <button
236
+ type="button"
237
+ onClick={() => {
238
+ pause();
239
+ goPrev();
240
+ }}
241
+ aria-label="Previous slide"
242
+ style={navButtonStyle("left")}
243
+ >
244
+
245
+ </button>
246
+
247
+ <button
248
+ type="button"
249
+ onClick={() => {
250
+ pause();
251
+ goNext();
252
+ }}
253
+ aria-label="Next slide"
254
+ style={navButtonStyle("right")}
255
+ >
256
+
257
+ </button>
258
+
259
+ {/* Thumbnail navigation overlay (inside viewport, outside slide node) */}
260
+ <div
261
+ style={{
262
+ position: "absolute",
263
+ right: 12,
264
+ bottom: 12,
265
+ zIndex: 2,
266
+ }}
267
+ >
268
+ <div style={{ display: "flex", gap: 10 }} aria-label="Choose a slide">
269
+ {items.map((item, 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}: ${item.title}`}
280
+ aria-current={isActive ? "true" : undefined}
281
+ style={thumbnailButtonStyle(isActive)}
282
+ >
283
+ <span
284
+ aria-hidden="true"
285
+ style={{
286
+ display: "block",
287
+ width: "100%",
288
+ height: "100%",
289
+ borderRadius: 10,
290
+ backgroundImage: item.thumbnail ? `url(${item.thumbnail})` : undefined,
291
+ backgroundSize: "cover",
292
+ backgroundPosition: "center",
293
+ }}
294
+ />
295
+ </button>
296
+ );
297
+ })}
298
+ </div>
299
+ </div>
300
+ </div>
301
+ </section>
302
+ );
303
+ }
304
+
305
+ function navButtonStyle(side) {
306
+ return {
307
+ position: "absolute",
308
+ top: "50%",
309
+ transform: "translateY(-50%)",
310
+ [side]: 12,
311
+ width: 44,
312
+ height: 44,
313
+ borderRadius: 12,
314
+ border: "1px solid rgba(255,255,255,0.25)",
315
+ background: "rgba(0,0,0,0.45)",
316
+ color: "#fff",
317
+ display: "grid",
318
+ placeItems: "center",
319
+ cursor: "pointer",
320
+ fontSize: 28,
321
+ lineHeight: 1,
322
+ };
323
+ }
324
+
325
+ const pauseButtonStyle = {
326
+ position: "absolute",
327
+ top: 12,
328
+ left: 12,
329
+ zIndex: 2,
330
+ width: 40,
331
+ height: 40,
332
+ borderRadius: 10,
333
+ border: "1px solid rgba(255,255,255,0.25)",
334
+ background: "rgba(0,0,0,0.6)",
335
+ color: "#fff",
336
+ display: "grid",
337
+ placeItems: "center",
338
+ cursor: "pointer",
339
+ };
340
+
341
+ function thumbnailButtonStyle(active) {
342
+ return {
343
+ width: 72,
344
+ height: 44,
345
+ padding: 0,
346
+ borderRadius: 10,
347
+ border: active ? "2px solid #111" : "1px solid rgba(0,0,0,0.35)",
348
+ background: active ? "rgba(0,0,0,0.06)" : "transparent",
349
+ cursor: "pointer",
350
+ };
351
+ }
352
+
353
+ const DEFAULT_ITEMS = [
354
+ {
355
+ title: "Neighbors",
356
+ description: "A chaotic dispute spirals. Watch the latest episode now.",
357
+ href: "#",
358
+ image: "https://picsum.photos/seed/hero-1/1200/600",
359
+ thumbnail: "https://picsum.photos/seed/thumb-1/240/140",
360
+ },
361
+ {
362
+ title: "UCLA at Michigan",
363
+ description: "Tip-off at 12:45 PM ET. Catch it live.",
364
+ href: "#",
365
+ image: "https://picsum.photos/seed/hero-2/1200/600",
366
+ thumbnail: "https://picsum.photos/seed/thumb-2/240/140",
367
+ },
368
+ {
369
+ title: "Fire Country",
370
+ description: "A risky mission tests loyalties and nerves.",
371
+ href: "#",
372
+ image: "https://picsum.photos/seed/hero-3/1200/600",
373
+ thumbnail: "https://picsum.photos/seed/thumb-3/240/140",
374
+ },
375
+ ];
376
+ ```
377
+
378
+ ## Acceptance Checks
379
+ - Semantics:
380
+ - The carousel container has `aria-roledescription="carousel"` and an accessible name.
381
+ - Each slide has `aria-roledescription="slide"` and exposes position (e.g., "2 of 3").
382
+ - Content of non-visible slides is not reachable by keyboard or screen reader.
383
+ - Autoplay:
384
+ - With `prefers-reduced-motion: reduce`, autoplay is paused by default.
385
+ - Tabbing into the carousel pauses autoplay.
386
+ - Autoplay does not run while paused.
387
+ - Controls (keyboard):
388
+ - Previous and Next buttons are reachable via Tab and move one slide per activation.
389
+ - Each thumbnail is reachable via Tab and activates its corresponding slide.
390
+ - Each thumbnail's `aria-label` includes not only "Go to slide N" but also a simple title or name for the corresponding slide.
391
+ - The active thumbnail exposes state (e.g., `aria-current="true"`).
392
+ - Content:
393
+ - Each slide includes a visible title (`<h2>`), a short description, and one primary CTA link.
394
+ - Screen reader:
395
+ - When changing slides while paused, the carousel name and slide position are announced without duplicate announcements.
@@ -0,0 +1,179 @@
1
+ ---
2
+ id: collection-row.basic
3
+ title: Collection Row
4
+ stack: web/react
5
+ status: beta
6
+ latest_version: 0.2.0
7
+ tags: [collection-row, shelf, rail, horizontal-list, ecommerce, navigation]
8
+ aliases: [content row, content rail, rail, strip, shelf, multi item carousel, product row, product card grid, product cards, card row, media row]
9
+ summary: Horizontal product shelf with a heading, list semantics, and Prev/Next paging that moves focus to newly revealed items.
10
+ ---
11
+
12
+ # Collection Row
13
+
14
+ Pattern ID: `collection-row.basic`
15
+
16
+ Horizontal product shelf with a heading, list semantics, and Prev/Next paging that moves focus to newly revealed items.
17
+
18
+ ## Use When
19
+ - Use when displaying multiple related items in a horizontally scrollable row under a shared category heading (e.g., "Customers Also Viewed", "Action Movies").
20
+ - Use when multiple items are visible simultaneously and can be scrolled left or right.
21
+ - Use when each item is a compact card with a primary visual element and brief supporting text.
22
+
23
+ ## Do Not Use When
24
+ - Do not use when only one item is visible at a time within a rotatable sequence (use `carousel`).
25
+ - Do not use when items are arranged in a multi-row or multi-column layout (use `grid`).
26
+ - Do not use when items are presented as a simple vertical list without horizontal scrolling (use `list`).
27
+
28
+ ## Must Haves
29
+ - Use a visible heading, typically an `<h2>`, above the row.
30
+ - Use list semantics for the row: `ul` with `li` items.
31
+ - Each item must comprise a single focus stop: in other words, consist of a single link `<a>` that contains:
32
+ - A visual element (image, poster, thumbnail, or media preview).
33
+ - A visible title that identifies the item.
34
+ - Optional visible metadata (e.g., price, episode number, rating).
35
+ - Each item link must have an accessible name composed of:
36
+ - title + metadata (optional) via `aria-labelledby`
37
+ - Each item link should expose position context (e.g., "3 of 18") as supplemental information:
38
+ - Provide an offscreen "X of Y" element.
39
+ - Reference it via `aria-describedby`.
40
+ - The position must reflect the item's index within the full set, not just the currently visible subset.
41
+ - Provide paging controls:
42
+ - Next button on the right edge of the row container (vertically centered)
43
+ - Previous button on the left edge when not on the first page
44
+ - Paging focus behavior:
45
+ - Activating Next moves focus to the first newly revealed item (left-most visible link).
46
+ - For example, if items 1 through 6 are visible, and the user activates the Next button, then items 7 through 12 become visible, and focus moves to item 7.
47
+ - Activating Previous moves focus to the last newly revealed item (right-most visible link).
48
+ - Ensure a visible focus state (e.g., a 2px solid outline offset by 1-2px) on each item and button.
49
+
50
+ ## Customizable
51
+ - In the golden pattern, we wrap the component in a container with `role="group"` and `aria-labelledby` pointing to the heading ID. This is optional. Engineers may choose instead to use a `<section>` or `role="region"`, or to eschew the container entirely.
52
+ - Items must at minimum have some "title" text that gives each item a name, but they are not required to also have metadata, like a price, or rating, etc.
53
+
54
+ ## Don'ts
55
+ - Do not let Tab from the last visible item move into out-of-view items; it must reach the Next button instead.
56
+ - Do not split the item into multiple separate interactive elements (one item = one link).
57
+ - Do not rely solely on poster art or imagery to communicate the name of each item.
58
+
59
+ ## Golden Pattern
60
+
61
+ Structural reference for AI coding assistants — semantics, focus, and keyboard behavior. Styling, copy, and demo data are illustrative.
62
+
63
+ ```jsx
64
+ "use client";
65
+
66
+ export function CollectionRow({ heading = "Customers Also Viewed", items = ITEMS, pageSize = 4 }) {
67
+ const headingId = useId();
68
+ const [startIndex, setStartIndex] = useState(0);
69
+
70
+ const linkRefs = useRef([]);
71
+
72
+ const total = items.length;
73
+ const endIndex = Math.min(startIndex + pageSize, total);
74
+ const visible = items.slice(startIndex, endIndex);
75
+
76
+ const canPrev = startIndex > 0;
77
+ const canNext = endIndex < total;
78
+
79
+ function goNext() {
80
+ if (!canNext) return;
81
+ const nextStart = Math.min(startIndex + pageSize, Math.max(total - pageSize, 0));
82
+ setStartIndex(nextStart);
83
+ requestAnimationFrame(() => linkRefs.current[0]?.focus());
84
+ }
85
+
86
+ function goPrev() {
87
+ if (!canPrev) return;
88
+ const prevStart = Math.max(startIndex - pageSize, 0);
89
+ setStartIndex(prevStart);
90
+ requestAnimationFrame(() => linkRefs.current[visible.length - 1]?.focus());
91
+ }
92
+
93
+ return (
94
+ <div role="group" aria-labelledby={headingId}>
95
+ <h2 id={headingId}>{heading}</h2>
96
+
97
+ {canPrev && (
98
+ <button type="button" onClick={goPrev} aria-label="Previous items">
99
+ Prev
100
+ </button>
101
+ )}
102
+
103
+ <ul>
104
+ {visible.map((item, i) => {
105
+ const globalIndex = startIndex + i;
106
+ const titleId = `${headingId}-title-${globalIndex}`;
107
+ const metaId = `${headingId}-meta-${globalIndex}`;
108
+ const posId = `${headingId}-pos-${globalIndex}`;
109
+
110
+ return (
111
+ <li key={item.id}>
112
+ <a
113
+ href={item.href}
114
+ ref={(el) => (linkRefs.current[i] = el)}
115
+ aria-labelledby={`${titleId} ${metaId}`}
116
+ aria-describedby={posId}
117
+ >
118
+ <span aria-hidden="true">[image]</span>
119
+ <div id={titleId}>{item.title}</div>
120
+ <span id={posId} style={srOnly}>
121
+ {globalIndex + 1} of {total}
122
+ </span>
123
+ <div id={metaId}>{item.meta}</div>
124
+ </a>
125
+ </li>
126
+ );
127
+ })}
128
+ </ul>
129
+
130
+ {canNext && (
131
+ <button type="button" onClick={goNext} aria-label="Next items">
132
+ Next
133
+ </button>
134
+ )}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ // Visually-hidden styles matching the global sr-only utility (global.sr-only).
140
+ const srOnly = {
141
+ position: "absolute",
142
+ width: 1,
143
+ height: 1,
144
+ padding: 0,
145
+ margin: -1,
146
+ overflow: "hidden",
147
+ clip: "rect(0,0,0,0)",
148
+ whiteSpace: "nowrap",
149
+ border: 0,
150
+ };
151
+
152
+ const ITEMS = [
153
+ { id: "1", title: "Item One", meta: "$24.95", href: "#" },
154
+ { id: "2", title: "Item Two", meta: "$29.00", href: "#" },
155
+ { id: "3", title: "Item Three", meta: "$18.50", href: "#" },
156
+ { id: "4", title: "Item Four", meta: "$22.00", href: "#" },
157
+ { id: "5", title: "Item Five", meta: "$27.99", href: "#" },
158
+ { id: "6", title: "Item Six", meta: "$16.95", href: "#" },
159
+ { id: "7", title: "Item Seven", meta: "$25.50", href: "#" },
160
+ { id: "8", title: "Item Eight", meta: "$34.00", href: "#" },
161
+ ];
162
+ ```
163
+
164
+ ## Acceptance Checks
165
+ - Structure:
166
+ - A visible heading is present.
167
+ - The row uses `ul` / `li` semantics.
168
+ - Each item is a single link wrapping its content.
169
+ - Accessible naming:
170
+ - Each link exposes a programmatic name that includes the visible title.
171
+ - If metadata is present, it contributes to the accessible name.
172
+ - Each link exposes position context (e.g., "3 of 18") once via `aria-describedby`.
173
+ - Keyboard:
174
+ - Tab order reaches Previous and Next buttons without forcing navigation through hidden items.
175
+ - Activating Next moves focus to the first newly visible item.
176
+ - Activating Previous moves focus to the last newly visible item.
177
+ - Tabbing from the last visible item moves to the Next button (not to hidden items).
178
+ - Visual focus:
179
+ - All interactive elements (item links and paging buttons) have a visible focus indicator.