@hegemonart/get-design-done 1.15.0 → 1.18.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 (70) hide show
  1. package/.claude-plugin/marketplace.json +9 -5
  2. package/.claude-plugin/plugin.json +19 -5
  3. package/CHANGELOG.md +122 -0
  4. package/README.md +41 -0
  5. package/SKILL.md +4 -1
  6. package/agents/component-benchmark-harvester.md +112 -0
  7. package/agents/component-benchmark-synthesizer.md +88 -0
  8. package/agents/design-auditor.md +60 -1
  9. package/agents/design-doc-writer.md +21 -0
  10. package/agents/design-executor.md +22 -4
  11. package/agents/design-pattern-mapper.md +61 -0
  12. package/agents/motion-mapper.md +74 -9
  13. package/agents/token-mapper.md +8 -0
  14. package/connections/design-corpora.md +158 -0
  15. package/package.json +13 -3
  16. package/reference/components/README.md +94 -0
  17. package/reference/components/TEMPLATE.md +184 -0
  18. package/reference/components/accordion.md +217 -0
  19. package/reference/components/alert.md +198 -0
  20. package/reference/components/badge.md +202 -0
  21. package/reference/components/breadcrumbs.md +198 -0
  22. package/reference/components/button.md +195 -0
  23. package/reference/components/card.md +200 -0
  24. package/reference/components/checkbox.md +207 -0
  25. package/reference/components/chip.md +209 -0
  26. package/reference/components/command-palette.md +228 -0
  27. package/reference/components/date-picker.md +227 -0
  28. package/reference/components/drawer.md +201 -0
  29. package/reference/components/file-upload.md +219 -0
  30. package/reference/components/input.md +208 -0
  31. package/reference/components/label.md +200 -0
  32. package/reference/components/link.md +193 -0
  33. package/reference/components/list.md +217 -0
  34. package/reference/components/menu.md +212 -0
  35. package/reference/components/modal-dialog.md +210 -0
  36. package/reference/components/navbar.md +211 -0
  37. package/reference/components/pagination.md +205 -0
  38. package/reference/components/popover.md +197 -0
  39. package/reference/components/progress.md +210 -0
  40. package/reference/components/radio.md +203 -0
  41. package/reference/components/rich-text-editor.md +226 -0
  42. package/reference/components/select-combobox.md +219 -0
  43. package/reference/components/sidebar.md +211 -0
  44. package/reference/components/skeleton.md +197 -0
  45. package/reference/components/slider.md +208 -0
  46. package/reference/components/stepper.md +220 -0
  47. package/reference/components/switch.md +194 -0
  48. package/reference/components/table.md +229 -0
  49. package/reference/components/tabs.md +213 -0
  50. package/reference/components/toast.md +200 -0
  51. package/reference/components/tooltip.md +201 -0
  52. package/reference/components/tree.md +225 -0
  53. package/reference/css-grid-layout.md +835 -0
  54. package/reference/external/NOTICE.hyperframes +28 -0
  55. package/reference/image-optimization.md +582 -0
  56. package/reference/motion-advanced.md +754 -0
  57. package/reference/motion-easings.md +381 -0
  58. package/reference/motion-interpolate.md +282 -0
  59. package/reference/motion-spring.md +234 -0
  60. package/reference/motion-transition-taxonomy.md +155 -0
  61. package/reference/motion.md +20 -0
  62. package/reference/output-contracts/motion-map.schema.json +135 -0
  63. package/reference/registry.json +285 -0
  64. package/reference/registry.schema.json +6 -1
  65. package/reference/variable-fonts-loading.md +532 -0
  66. package/scripts/lib/easings.cjs +280 -0
  67. package/scripts/lib/parse-contract.cjs +220 -0
  68. package/scripts/lib/spring.cjs +160 -0
  69. package/scripts/tests/test-motion-provenance.sh +64 -0
  70. package/skills/benchmark/SKILL.md +105 -0
@@ -0,0 +1,210 @@
1
+ # Modal / Dialog — Benchmark Spec
2
+
3
+ **Harvested from**: Radix UI Dialog, WAI-ARIA APG, Material 3, Atlassian, Carbon, Mantine, shadcn/ui, Fluent 2
4
+ **Wave**: 2 · **Category**: Containers
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A modal dialog is a blocking overlay that requires user interaction before returning to the main content. It is rendered in a portal above the page, traps keyboard focus within itself, prevents interaction with the background, and closes on Escape. Use modals sparingly — they interrupt flow. Prefer inline feedback or slide-out drawers for non-critical workflows. *(Material 3, Atlassian, Polaris all advise modal restraint)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ ┌─ Backdrop (aria-hidden) ──────────────────────────────────┐
18
+ │ │
19
+ │ ┌─ Dialog (role="dialog") ───────────────────────────┐ │
20
+ │ │ Title (aria-labelledby) [✕ Close] │ │
21
+ │ │─────────────────────────────────────────────────────│ │
22
+ │ │ Content / Body │ │
23
+ │ │─────────────────────────────────────────────────────│ │
24
+ │ │ [Cancel] [Confirm action] ← action footer │ │
25
+ │ └─────────────────────────────────────────────────────┘ │
26
+ └────────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ | Part | Required | Notes |
30
+ |------|----------|-------|
31
+ | Backdrop | Yes | `aria-hidden="true"` overlay; blocks pointer |
32
+ | Dialog container | Yes | `role="dialog"` + `aria-modal="true"` |
33
+ | Title / heading | Yes | `id` referenced by `aria-labelledby` on dialog |
34
+ | Close button | Yes | Keyboard accessible; returns focus to trigger |
35
+ | Body content | Yes | Scrollable if content exceeds viewport |
36
+ | Action footer | Conditional | Confirm + cancel pattern |
37
+ | Portal | Yes | Rendered outside normal DOM flow; `document.body` target |
38
+
39
+ ---
40
+
41
+ ## Variants
42
+
43
+ | Variant | Description | Systems |
44
+ |---------|-------------|---------|
45
+ | Default | Centered, backdrop, standard size | All |
46
+ | Alert dialog | Blocking confirmation; `role="alertdialog"` | WAI-ARIA APG, Material 3, Carbon |
47
+ | Full-page | Mobile-first; occupies full viewport | Material 3, Atlassian |
48
+ | Small / confirm | Narrow; 2-button pattern | Material 3, Carbon, shadcn |
49
+ | Large / content | Wide; for complex forms or media | Atlassian, Fluent |
50
+ | Scrollable content | Body scrolls; header/footer sticky | All |
51
+
52
+ **Norm** (≥6/18): Escape closes; backdrop click may close (configurable); focus trapped inside.
53
+ **Diverge**: backdrop-click-to-close — Material 3 and shadcn default to close; Atlassian and Carbon recommend NOT closing on backdrop click to prevent accidental dismissal of forms.
54
+
55
+ ---
56
+
57
+ ## States
58
+
59
+ | State | ARIA |
60
+ |-------|------|
61
+ | Open | `aria-modal="true"` on dialog; `inert` on `<body>` content (or equivalent) |
62
+ | Closed | Dialog removed from DOM or `display:none`; focus returned to trigger |
63
+ | Loading content | `aria-busy="true"` on dialog body |
64
+
65
+ ---
66
+
67
+ ## Sizing & Spacing
68
+
69
+ | Size | Width | Max height | Notes |
70
+ |------|-------|------------|-------|
71
+ | sm | 400px | 80vh | Confirm dialogs |
72
+ | md (default) | 560px | 80vh | Standard |
73
+ | lg | 720px | 90vh | Complex forms |
74
+ | full | 100vw/100vh | — | Mobile sheet pattern |
75
+
76
+ Padding: 24px (header/footer), 24px (body horizontal), 20px (body vertical).
77
+ Body scroll: `overflow-y: auto` with `overscroll-behavior: contain`.
78
+
79
+ Cross-link: `reference/surfaces.md` — concentric radius, elevation (shadow-as-border)
80
+
81
+ ---
82
+
83
+ ## Typography
84
+
85
+ - Title: 18–20px/600; `id` attribute set for `aria-labelledby`
86
+ - Body: 14–16px/400
87
+ - Description (if separate from body): 14px/400 muted; linked via `aria-describedby` on dialog
88
+
89
+ ---
90
+
91
+ ## Keyboard & Accessibility
92
+
93
+ > **WAI-ARIA role**: `dialog` (or `alertdialog` for blocking confirmations)
94
+ > **Required attributes**: `aria-modal="true"`, `aria-labelledby` (dialog title id), optionally `aria-describedby`
95
+
96
+ ### Keyboard Contract
97
+
98
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ — W3C — 2024*
99
+
100
+ | Key | Action |
101
+ |-----|--------|
102
+ | Tab | Moves focus to next focusable element inside dialog (wraps from last to first) |
103
+ | Shift+Tab | Moves focus to previous focusable element inside dialog (wraps from first to last) |
104
+ | Escape | Closes the dialog and returns focus to the element that opened it |
105
+
106
+ ### Focus Management
107
+
108
+ 1. **On open**: focus moves to the first focusable element inside the dialog (or to the dialog itself if no focusable children)
109
+ 2. **While open**: Tab/Shift+Tab cycle only within the dialog — focus MUST NOT leave the dialog
110
+ 3. **On close**: focus MUST return to the element that triggered the dialog open
111
+
112
+ ### Accessibility Rules
113
+
114
+ - `aria-modal="true"` MUST be set — tells AT to ignore background content (supplement with `inert` attribute on background for browsers without full `aria-modal` support)
115
+ - Dialog title MUST have an `id` referenced by `aria-labelledby` — screen reader announces "Dialog: [title]" on open
116
+ - `role="alertdialog"` for confirmation dialogs where the user must respond (delete confirmations, logout confirmation)
117
+ - Scroll-lock: prevent `<body>` scroll when dialog is open (`overflow: hidden` on `<body>`)
118
+ - Portal: render dialog in `document.body` to escape stacking context issues (z-index isolation)
119
+
120
+ ---
121
+
122
+ ## Motion
123
+
124
+ | Transition | Duration | Easing | Notes |
125
+ |------------|----------|--------|-------|
126
+ | Backdrop fade in | 200ms | ease-out | opacity 0→0.5 |
127
+ | Dialog enter | 200ms | ease-out | scale 0.95→1 + fade |
128
+ | Dialog exit | 150ms | ease-in | scale 1→0.95 + fade |
129
+ | Backdrop fade out | 150ms | ease-in | — |
130
+
131
+ Use `AnimatePresence` (Framer Motion) or `data-state` + CSS for mount/unmount animation.
132
+ Cross-link: `reference/motion.md` — `AnimatePresence initial={false}`, `prefers-reduced-motion`
133
+
134
+ ---
135
+
136
+ ## Do / Don't
137
+
138
+ ### Do
139
+ - Return focus to the triggering element on close *(WAI-ARIA APG, all systems)*
140
+ - Trap focus inside the dialog while open *(WAI-ARIA APG)*
141
+ - Render in a portal at `document.body` *(Radix, Mantine, shadcn)*
142
+ - Set `overflow:hidden` on `<body>` to prevent background scroll *(Material 3, Carbon)*
143
+
144
+ ### Don't
145
+ - Don't close on backdrop click for dialogs with form input — data loss risk *(Atlassian, Carbon)*
146
+ - Don't use `role="dialog"` without `aria-labelledby` — dialog is announced without a name *(WAI-ARIA APG)*
147
+ - Don't use `display:none` to hide a dialog — use DOM removal or `hidden` attribute for correct AT behavior *(WAI-ARIA APG)*
148
+ - Don't stack more than 2 dialogs — use a single dialog with internal step navigation *(Material 3, Atlassian)*
149
+
150
+ ---
151
+
152
+ ## Anti-patterns Cross-links
153
+
154
+ | Anti-pattern | Entry |
155
+ |--------------|-------|
156
+ | Focus not trapped in modal | `reference/anti-patterns.md` |
157
+ | No focus return on close | `reference/anti-patterns.md` |
158
+
159
+ ---
160
+
161
+ ## Benchmark Citations
162
+
163
+ | Claim | Sources |
164
+ |-------|---------|
165
+ | Escape closes dialog | WAI-ARIA APG §4.1, all 8 systems |
166
+ | Focus trap (Tab wraps inside) | WAI-ARIA APG §4.1 |
167
+ | aria-modal="true" required | WAI-ARIA APG |
168
+ | Portal at document.body | Radix, Mantine, shadcn |
169
+ | role="alertdialog" for confirmations | WAI-ARIA APG, Material 3 |
170
+ | backdrop-click: configurable | Material 3, shadcn (default: close); Atlassian, Carbon (default: stay) |
171
+
172
+ Full system URLs: `connections/design-corpora.md`
173
+
174
+ ---
175
+
176
+ ## Grep Signatures
177
+
178
+ ```bash
179
+ # Dialog without aria-labelledby
180
+ grep -rn 'role="dialog"\|role="alertdialog"' src/ | grep -v 'aria-labelledby'
181
+
182
+ # Dialog missing aria-modal
183
+ grep -rn 'role="dialog"' src/ | grep -v 'aria-modal'
184
+
185
+ # Modal without focus trap
186
+ grep -rn 'modal\|dialog' src/ | grep -L 'FocusTrap\|useFocusTrap\|focus-trap\|inert'
187
+
188
+ # Body scroll not locked on modal open
189
+ grep -rn 'modal.*open\|isOpen.*modal' src/ | grep -v 'overflow\|scroll-lock\|body\.'
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Failing Example
195
+
196
+ ```html
197
+ <!-- BAD: dialog with no focus trap, no aria-modal, no aria-labelledby -->
198
+ <div class="modal" style="display:block">
199
+ <div class="modal-content">
200
+ <h2>Confirm deletion</h2>
201
+ <p>This action cannot be undone.</p>
202
+ <button onclick="close()">Cancel</button>
203
+ <button onclick="confirm()">Delete</button>
204
+ </div>
205
+ </div>
206
+ ```
207
+
208
+ **Why it fails**: No `role="dialog"`, no `aria-modal`, no `aria-labelledby` — screen readers cannot announce the dialog name or suppress background content. Tab escapes the modal. Escape does nothing.
209
+ **Grep detection**: `grep -rn 'class.*modal\|class.*dialog' src/ | grep -v 'role=\|aria-'`
210
+ **Fix**: Use Radix `<Dialog>` or implement WAI-ARIA dialog pattern with `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus trap, Escape handler, and portal rendering.
@@ -0,0 +1,211 @@
1
+ # Navbar (Top Navigation Bar) — Benchmark Spec
2
+
3
+ **Harvested from**: Material 3, Carbon, Polaris, Atlassian, Primer, UUPM (app-interface, MIT)
4
+ **Wave**: 4 · **Category**: Navigation
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A navbar is the primary horizontal navigation surface at the top of an application. It houses the logo/home link, primary navigation destinations, and secondary actions (search, notifications, profile). It communicates the application's identity and provides always-visible wayfinding. Differs from a Sidebar (vertical, secondary nav) and Breadcrumb (trail-based context). *(Material 3, Carbon, Polaris, Atlassian agree: top navbar = primary navigation + brand identity)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ ┌──────────────────────────────────────────────────────┐ role="banner"
18
+ │ [Skip to main] (visually hidden, focus-visible) │
19
+ │ [Logo/Home] | Nav Item · Nav Item · Nav Item | [🔍][👤]│ role="navigation" aria-label="Primary"
20
+ └──────────────────────────────────────────────────────┘
21
+ ```
22
+
23
+ | Part | Required | Notes |
24
+ |------|----------|-------|
25
+ | `<header>` wrapper | Yes | `role="banner"` — one per page |
26
+ | `<nav>` | Yes | `role="navigation"` + `aria-label="Primary"` |
27
+ | Skip-to-main link | Yes | First focusable element; `href="#main-content"`; visible on focus |
28
+ | Logo / home link | Yes | `<a href="/">` with `aria-label="[App name] home"` |
29
+ | Nav items | Yes | `<a>` for routing; `aria-current="page"` on active item |
30
+ | Secondary actions | No | Icon buttons (search, notifications, profile) |
31
+ | Hamburger button | Conditional | Mobile only; `aria-expanded` + `aria-controls` |
32
+ | Scroll shadow | No | `box-shadow` appears on scroll > 0px |
33
+
34
+ ---
35
+
36
+ ## Variants
37
+
38
+ | Variant | Description | Systems |
39
+ |---------|-------------|---------|
40
+ | Default / static | Fixed in document flow; scrolls with page | Carbon, Polaris |
41
+ | Sticky / fixed | `position: sticky` or `fixed`; stays at top on scroll | Material 3, Atlassian, Primer |
42
+ | Transparent hero | Transparent over hero image; becomes opaque on scroll | Material 3, Polaris |
43
+ | Compact / dense | Reduced height (48px) for data-heavy apps | Carbon, UUPM app-interface |
44
+ | Dashboard navbar | App-switcher + user avatar + notifications | UUPM app-interface (MIT) |
45
+ | Settings navbar | Breadcrumb-style subtitle below app name | UUPM app-interface (MIT) |
46
+
47
+ **Norm** (≥4 systems agree): 56–64px height; logo left-aligned; secondary actions right-aligned.
48
+ **Diverge**: Material 3 distinguishes "Top app bar" (mobile) from "Navigation bar" (desktop) as separate components; other systems use a single responsive navbar.
49
+
50
+ ---
51
+
52
+ ## States
53
+
54
+ | State | Trigger | Visual | ARIA |
55
+ |-------|---------|--------|------|
56
+ | default | — | Full nav visible, no shadow | — |
57
+ | scrolled | scroll > 0 | `box-shadow` bottom border appears | — |
58
+ | nav-item-hover | pointer over item | underline or bg highlight | — |
59
+ | nav-item-focus | keyboard focus | 2px focus-visible ring | — |
60
+ | nav-item-active | current route | `font-weight: 600` + indicator underline or pill | `aria-current="page"` |
61
+ | mobile-collapsed | viewport < breakpoint | Nav items hidden; hamburger visible | `aria-expanded="false"` on hamburger |
62
+ | mobile-expanded | hamburger activated | Nav items shown as vertical list | `aria-expanded="true"` on hamburger |
63
+
64
+ ---
65
+
66
+ ## Sizing & Spacing
67
+
68
+ | Size | Height | Logo H | Item padding H | Font |
69
+ |------|--------|--------|----------------|------|
70
+ | compact | 48px | 24px | 12px | 13px/500 |
71
+ | default | 56px | 28px | 16px | 14px/500 |
72
+ | comfortable | 64px | 32px | 20px | 15px/500 |
73
+
74
+ **Norm**: 56px default height (Material 3, Carbon, Polaris, Atlassian all converge here).
75
+ Mobile breakpoint: collapse at ≤768px (Carbon, Polaris); ≤960px for complex navbars (Atlassian).
76
+
77
+ ---
78
+
79
+ ## Typography
80
+
81
+ - Nav item labels: body-sm to body-md (13–15px), weight 500 for active, 400 for inactive
82
+ - Active item visual distinction must not rely on color alone (add font-weight or underline indicator)
83
+ - Secondary action icons: 20px, with `aria-label` on each button
84
+ - Skip link text: `body-sm`, matches surrounding text — it only needs to be visible on focus
85
+
86
+ Cross-link: `reference/typography.md` — body scale, weight tokens
87
+
88
+ ---
89
+
90
+ ## Keyboard & Accessibility
91
+
92
+ > **WAI-ARIA role**: `banner` (header), `navigation` (nav)
93
+ > **Required attributes**: `aria-label="Primary"` on `<nav>`; `aria-current="page"` on active item; `aria-expanded` + `aria-controls` on hamburger
94
+
95
+ ### Keyboard Contract
96
+
97
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/landmark-regions/ — W3C — 2024*
98
+
99
+ | Key | Action |
100
+ |-----|--------|
101
+ | Tab | Moves focus through focusable nav elements in DOM order |
102
+ | Shift+Tab | Moves focus backwards |
103
+ | Enter / Space | Activates focused link or button |
104
+ | Escape | Closes mobile expanded menu; returns focus to hamburger |
105
+
106
+ ### Accessibility Rules
107
+
108
+ - `<nav>` MUST have `aria-label="Primary"` — multiple `<nav>` landmarks on a page must all be distinctly labelled
109
+ - Skip-to-main link MUST be the first focusable element on the page — keyboard users need to bypass repetitive nav
110
+ - Active nav item MUST use `aria-current="page"` — color alone is insufficient for AT users
111
+ - Hamburger button MUST have `aria-expanded` and `aria-controls` — AT users need to know the nav state
112
+ - `role="banner"` MUST appear only once per page (one `<header>` at page level)
113
+
114
+ Cross-link: `reference/accessibility.md` — landmark regions, skip navigation
115
+
116
+ ---
117
+
118
+ ## Motion
119
+
120
+ | Transition | Duration | Easing | Notes |
121
+ |------------|----------|--------|-------|
122
+ | Scroll shadow appear | 150ms | ease-out | `box-shadow` opacity only |
123
+ | Mobile menu open | 200ms | ease-out | Height expand or slide-down |
124
+ | Mobile menu close | 150ms | ease-in | Collapse |
125
+ | Transparent → opaque on scroll | 200ms | ease-out | Background-color transition |
126
+
127
+ **BAN**: Animating navbar height on scroll — causes layout reflow and jank on every scroll event.
128
+
129
+ Cross-link: `reference/motion.md` — layout-affecting transitions
130
+
131
+ ---
132
+
133
+ ## Do / Don't
134
+
135
+ ### Do
136
+ - Include a visible skip-to-main link as the first focusable element *(WCAG 2.4.1, Carbon, Polaris)*
137
+ - Label `<nav>` with `aria-label="Primary"` *(WAI-ARIA APG landmark regions)*
138
+ - Use `aria-current="page"` on the active nav item *(WAI-ARIA, Atlassian)*
139
+ - Manage z-index explicitly on sticky navbars to prevent overlap issues *(Material 3, Carbon)*
140
+
141
+ ### Don't
142
+ - Don't use multiple unlabelled `<nav>` landmarks — screen reader users can't distinguish them *(WCAG 4.1.2)*
143
+ - Don't rely on color alone to indicate the active nav item *(WCAG 1.4.1)*
144
+ - Don't put more than 7 primary nav items — overwhelming and hard to scan *(Carbon, Polaris HIG)*
145
+ - Don't use `position: fixed` on mobile without accounting for virtual keyboard displacement
146
+
147
+ ---
148
+
149
+ ## Anti-patterns Cross-links
150
+
151
+ | Anti-pattern | Entry |
152
+ |--------------|-------|
153
+ | BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
154
+
155
+ ---
156
+
157
+ ## Benchmark Citations
158
+
159
+ | Claim | Sources |
160
+ |-------|---------|
161
+ | role="banner" on outer header | WAI-ARIA APG landmark regions |
162
+ | aria-label="Primary" required on nav | WAI-ARIA APG, WCAG 4.1.2 |
163
+ | Skip-to-main as first focusable element | WCAG 2.4.1, Carbon, Polaris |
164
+ | aria-current="page" on active item | WAI-ARIA APG, Atlassian, Primer |
165
+ | 56px default navbar height | Material 3, Carbon, Polaris, Atlassian |
166
+
167
+ Full system URLs: `connections/design-corpora.md`
168
+
169
+ ---
170
+
171
+ ## Grep Signatures
172
+
173
+ ```bash
174
+ # nav element missing aria-label
175
+ grep -rn '<nav' src/ | grep -v 'aria-label\|aria-labelledby'
176
+
177
+ # Active nav item missing aria-current
178
+ grep -rn 'active\|current\|selected' src/ | grep -i 'nav.*item\|navitem\|nav-link' | grep -v 'aria-current'
179
+
180
+ # Missing skip link
181
+ grep -rn 'skip.*main\|skip.*content\|skipnav' src/ | grep -v 'href'
182
+ # If the above returns nothing, no skip link exists
183
+ grep -rn 'id="main\|id="main-content"' src/ | head -5
184
+
185
+ # Multiple unlabelled nav landmarks
186
+ grep -rn '<nav' src/ | grep -v 'aria-label\|aria-labelledby'
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Failing Example
192
+
193
+ ```html
194
+ <!-- BAD: multiple <nav> elements without aria-label -->
195
+ <header>
196
+ <nav>
197
+ <a href="/">Home</a>
198
+ <a href="/about">About</a>
199
+ <a href="/contact" class="active">Contact</a> <!-- no aria-current -->
200
+ </nav>
201
+ </header>
202
+ <aside>
203
+ <nav> <!-- second nav, also unlabelled — ambiguous for AT users -->
204
+ <a href="/settings">Settings</a>
205
+ </nav>
206
+ </aside>
207
+ ```
208
+
209
+ **Why it fails**: Screen readers announce "navigation" twice with no way to distinguish them; active state relies on CSS class only (not `aria-current="page"`); no skip link; `<header>` lacks landmark context.
210
+ **Grep detection**: `grep -rn '<nav' src/ | grep -v 'aria-label'`
211
+ **Fix**: Add `aria-label="Primary"` and `aria-label="Secondary"` to each `<nav>`; add `aria-current="page"` to active link; add skip link as first child of `<header>`.
@@ -0,0 +1,205 @@
1
+ # Pagination — Benchmark Spec
2
+
3
+ **Harvested from**: Carbon, Polaris, Atlassian, Mantine, Material 3, UUPM (app-interface, MIT)
4
+ **Wave**: 4 · **Category**: Navigation
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ Pagination divides a large dataset into discrete pages and provides controls to navigate between them. It is the preferred pattern for server-rendered or API-paginated datasets where loading all items at once is impractical. Use infinite scroll for feeds/social content; use pagination for tables, search results, and list views where users need to reference or return to a specific page. *(Carbon, Polaris, Atlassian, Mantine all define pagination as discrete page-set navigation)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ <nav aria-label="Pagination">
18
+ [‹ Previous] [1] [2] [•3•] [4] [...] [24] [Next ›]
19
+ ↑ aria-current="page"
20
+ Items per page: [25 ▾] Showing 51–75 of 587 items
21
+ </nav>
22
+ ```
23
+
24
+ | Part | Required | Notes |
25
+ |------|----------|-------|
26
+ | `<nav>` container | Yes | `aria-label="Pagination"` |
27
+ | Previous button | Yes | `aria-label="Previous page"`; `disabled` on page 1 |
28
+ | Next button | Yes | `aria-label="Next page"`; `disabled` on last page |
29
+ | Page number buttons | Yes | Each button `aria-label="Page N"` |
30
+ | Current page indicator | Yes | `aria-current="page"` on active page button |
31
+ | Ellipsis | Conditional | At > 7 pages; `aria-hidden="true"` or `aria-label="More pages"` |
32
+ | Per-page selector | No | `<select>` with visible `<label>`; options: 10/25/50/100 |
33
+ | Results summary | No | "Showing 51–75 of 587 items"; `aria-live="polite"` on change |
34
+
35
+ ---
36
+
37
+ ## Variants
38
+
39
+ | Variant | Description | Systems |
40
+ |---------|-------------|---------|
41
+ | Full | Previous + page numbers + Next | Carbon, Polaris, Atlassian, Mantine |
42
+ | Compact | Previous + "Page N of M" label + Next (no individual page buttons) | Carbon, Material 3 |
43
+ | Simple | Previous / Next only (no page numbers) | Polaris, Atlassian mobile |
44
+ | With per-page | Adds items-per-page selector below or beside | Carbon, Polaris, UUPM list-view (MIT) |
45
+ | Borderless | Text-only Previous/Next; no page number buttons | Mantine |
46
+
47
+ **Norm** (≥4 systems agree): show 5–7 page buttons max when not truncating; always show first, last, ±1 around current when truncating.
48
+ **Diverge**: Carbon combines per-page selector and result count into the pagination bar; Polaris separates them.
49
+
50
+ ---
51
+
52
+ ## States
53
+
54
+ | State | Trigger | Visual | ARIA |
55
+ |-------|---------|--------|------|
56
+ | default | — | All buttons enabled except current page | — |
57
+ | current-page | — | Filled/highlighted button | `aria-current="page"` |
58
+ | prev-disabled | page = 1 | Previous button dimmed | `disabled` or `aria-disabled="true"` |
59
+ | next-disabled | page = last | Next button dimmed | `disabled` or `aria-disabled="true"` |
60
+ | button-hover | pointer over | 8% overlay | — |
61
+ | button-focus | keyboard focus | 2px focus-visible ring | — |
62
+ | loading | page change in flight | Spinner overlay on content; buttons retain focus | `aria-busy="true"` on results region |
63
+
64
+ ---
65
+
66
+ ## Sizing & Spacing
67
+
68
+ | Element | Size | Notes |
69
+ |---------|------|-------|
70
+ | Page button | 36×36px (md) | 32×32px compact; min tap target 44px via padding |
71
+ | Prev/Next button | 36px height × auto | Label text + chevron icon |
72
+ | Button gap | 4px | Between adjacent page buttons |
73
+ | Per-page select | 80px width | 4 options: 10/25/50/100 |
74
+ | Container padding | 16px V | Breathing room above/below |
75
+
76
+ **Norm**: 36px button height (Carbon, Atlassian, Mantine). 4px gap between buttons.
77
+
78
+ ---
79
+
80
+ ## Typography
81
+
82
+ - Page number buttons: body-sm (13–14px), weight 400; active page weight 600
83
+ - Previous/Next labels: body-sm, weight 400
84
+ - Results summary: body-sm, `color: --text-subtle`
85
+ - Per-page label: label-sm (12px), always visible above or beside the select
86
+
87
+ Cross-link: `reference/typography.md` — body-sm, label scale
88
+
89
+ ---
90
+
91
+ ## Keyboard & Accessibility
92
+
93
+ > **WAI-ARIA role**: `navigation` (container)
94
+ > **Required attributes**: `aria-label="Pagination"` on `<nav>`; `aria-current="page"` on active page button; `aria-label="Previous page"` / `"Next page"` on controls; `aria-label="Page N"` on each numbered button
95
+
96
+ ### Keyboard Contract
97
+
98
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/landmark-regions/ — W3C — 2024*
99
+
100
+ | Key | Action |
101
+ |-----|--------|
102
+ | Tab | Moves focus to next button in pagination |
103
+ | Shift+Tab | Moves focus to previous button |
104
+ | Enter / Space | Activates focused button (navigates to page or changes per-page count) |
105
+
106
+ ### Accessibility Rules
107
+
108
+ - `aria-label="Pagination"` MUST be on the `<nav>` — distinguishes from other navigation landmarks on the page
109
+ - `aria-current="page"` MUST be on the currently active page button — screen readers announce "Page 3, current"
110
+ - Previous button on page 1 and Next button on last page MUST use `disabled` or `aria-disabled="true"` + visually dimmed
111
+ - `aria-label` on each page number button ("Page 1", "Page 2") prevents screen readers from just announcing the number alone
112
+ - Results summary text MUST use `aria-live="polite"` so screen reader users hear the count update after page change
113
+ - Per-page `<select>` MUST have a visible `<label>` — not just a placeholder
114
+
115
+ Cross-link: `reference/accessibility.md` — aria-current, landmark labelling, live regions
116
+
117
+ ---
118
+
119
+ ## Motion
120
+
121
+ | Transition | Duration | Easing | Notes |
122
+ |------------|----------|--------|-------|
123
+ | Button hover | 80ms | ease-out | Background color only |
124
+ | Page change content transition | 150ms | ease-out | Fade the results region; managed by page, not pagination |
125
+ | Ellipsis expand (if interactive) | 120ms | ease-out | Reveal hidden page buttons |
126
+
127
+ **BAN**: Do not animate the pagination bar itself when a page changes — only the content region transitions.
128
+
129
+ Cross-link: `reference/motion.md` — content region transitions
130
+
131
+ ---
132
+
133
+ ## Do / Don't
134
+
135
+ ### Do
136
+ - Label the `<nav>` with `aria-label="Pagination"` *(WAI-ARIA APG, Carbon, Polaris)*
137
+ - Use `aria-current="page"` on the active page button *(WAI-ARIA APG)*
138
+ - Provide visible per-page selector with a visible label *(Carbon, Polaris, Atlassian)*
139
+ - Show a results summary ("Showing 51–75 of 587") near the pagination controls *(Carbon, Polaris)*
140
+
141
+ ### Don't
142
+ - Don't use `<a href>` with pushState without `aria-current` — screen readers won't know which page is current *(WAI-ARIA)*
143
+ - Don't show more than 7 page buttons without ellipsis truncation — visually overwhelming *(Carbon, Mantine)*
144
+ - Don't disable the Previous/Next buttons with only visual styling — use `disabled` attr or `aria-disabled` *(WCAG 1.4.3)*
145
+ - Don't place pagination at the top only — bottom placement is expected; both positions is acceptable *(Polaris, Carbon)*
146
+
147
+ ---
148
+
149
+ ## Anti-patterns Cross-links
150
+
151
+ | Anti-pattern | Entry |
152
+ |--------------|-------|
153
+ | BAN-07 | Missing `aria-current` on active navigation items — `reference/anti-patterns.md#ban-07` |
154
+
155
+ ---
156
+
157
+ ## Benchmark Citations
158
+
159
+ | Claim | Sources |
160
+ |-------|---------|
161
+ | aria-label="Pagination" on nav container | WAI-ARIA APG, Carbon, Polaris |
162
+ | aria-current="page" on active page button | WAI-ARIA APG, Atlassian, Mantine |
163
+ | Ellipsis pattern: first + last + ±1 around current | Carbon, Polaris, Atlassian, Mantine |
164
+ | Per-page selector requires visible label | WCAG 1.3.1, Carbon, Polaris |
165
+ | aria-live="polite" on results summary | WCAG 4.1.3, Carbon |
166
+
167
+ Full system URLs: `connections/design-corpora.md`
168
+
169
+ ---
170
+
171
+ ## Grep Signatures
172
+
173
+ ```bash
174
+ # Pagination nav missing aria-label
175
+ grep -rn 'pagination\|pager' src/ | grep '<nav' | grep -v 'aria-label'
176
+
177
+ # Active page button missing aria-current
178
+ grep -rn 'pagination\|pager' src/ | grep 'active\|current\|selected' | grep -v 'aria-current'
179
+
180
+ # Page buttons using <a> without aria-current
181
+ grep -rn '<a' src/ | grep -i 'page.*[0-9]\|pager' | grep -v 'aria-current'
182
+
183
+ # Per-page select missing label
184
+ grep -rn 'per.page\|items-per-page\|pageSize' src/ | grep '<select' | grep -v '<label\|aria-label'
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Failing Example
190
+
191
+ ```html
192
+ <!-- BAD: page buttons using <a href> with pushState but no aria-current -->
193
+ <nav> <!-- missing aria-label="Pagination" -->
194
+ <a href="?page=2" class="prev">Previous</a> <!-- no aria-label -->
195
+ <a href="?page=1">1</a>
196
+ <a href="?page=2">2</a>
197
+ <a href="?page=3" class="active">3</a> <!-- class active but no aria-current -->
198
+ <a href="?page=4">4</a>
199
+ <a href="?page=4" class="next">Next</a> <!-- no aria-label -->
200
+ </nav>
201
+ ```
202
+
203
+ **Why it fails**: No `aria-label` on `<nav>`; active page has CSS class but no `aria-current="page"` so AT announces "3" not "Page 3, current"; Previous/Next have no descriptive labels; no `aria-label="Page N"` on individual page links.
204
+ **Grep detection**: `grep -rn 'pagination' src/ | grep -v 'aria-current\|aria-label'`
205
+ **Fix**: Add `aria-label="Pagination"` to `<nav>`; add `aria-current="page"` to the active page button; add `aria-label="Previous page"` and `aria-label="Next page"`; add `aria-label="Page N"` to each numbered button.