@hegemonart/get-design-done 1.16.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 (49) hide show
  1. package/.claude-plugin/marketplace.json +7 -5
  2. package/.claude-plugin/plugin.json +17 -5
  3. package/CHANGELOG.md +84 -0
  4. package/README.md +20 -2
  5. package/agents/design-auditor.md +60 -1
  6. package/agents/design-doc-writer.md +21 -0
  7. package/agents/design-executor.md +22 -4
  8. package/agents/design-pattern-mapper.md +61 -0
  9. package/agents/motion-mapper.md +74 -9
  10. package/agents/token-mapper.md +8 -0
  11. package/package.json +10 -2
  12. package/reference/components/README.md +27 -23
  13. package/reference/components/alert.md +198 -0
  14. package/reference/components/badge.md +202 -0
  15. package/reference/components/breadcrumbs.md +198 -0
  16. package/reference/components/chip.md +209 -0
  17. package/reference/components/command-palette.md +228 -0
  18. package/reference/components/date-picker.md +227 -0
  19. package/reference/components/file-upload.md +219 -0
  20. package/reference/components/list.md +217 -0
  21. package/reference/components/menu.md +212 -0
  22. package/reference/components/navbar.md +211 -0
  23. package/reference/components/pagination.md +205 -0
  24. package/reference/components/progress.md +210 -0
  25. package/reference/components/rich-text-editor.md +226 -0
  26. package/reference/components/sidebar.md +211 -0
  27. package/reference/components/skeleton.md +197 -0
  28. package/reference/components/slider.md +208 -0
  29. package/reference/components/stepper.md +220 -0
  30. package/reference/components/table.md +229 -0
  31. package/reference/components/toast.md +200 -0
  32. package/reference/components/tree.md +225 -0
  33. package/reference/css-grid-layout.md +835 -0
  34. package/reference/external/NOTICE.hyperframes +28 -0
  35. package/reference/image-optimization.md +582 -0
  36. package/reference/motion-advanced.md +754 -0
  37. package/reference/motion-easings.md +381 -0
  38. package/reference/motion-interpolate.md +282 -0
  39. package/reference/motion-spring.md +234 -0
  40. package/reference/motion-transition-taxonomy.md +155 -0
  41. package/reference/motion.md +20 -0
  42. package/reference/output-contracts/motion-map.schema.json +135 -0
  43. package/reference/registry.json +183 -0
  44. package/reference/registry.schema.json +4 -0
  45. package/reference/variable-fonts-loading.md +532 -0
  46. package/scripts/lib/easings.cjs +280 -0
  47. package/scripts/lib/parse-contract.cjs +220 -0
  48. package/scripts/lib/spring.cjs +160 -0
  49. package/scripts/tests/test-motion-provenance.sh +64 -0
@@ -0,0 +1,219 @@
1
+ # File Upload — Benchmark Spec
2
+
3
+ **Harvested from**: Polaris (DropZone), Carbon (FileUploader), Atlassian Design System, Material 3
4
+ **Wave**: 5 · **Category**: Advanced
5
+ **Spec file**: `reference/components/file-upload.md`
6
+
7
+ ---
8
+
9
+ ## Purpose
10
+
11
+ A File Upload component lets users attach one or more files by dragging them onto a drop zone or clicking to open the native file picker. It must work for all users: keyboard-only users activate the hidden-but-accessible `<input type="file">`, while pointer users can drag-drop. A file list tracks upload progress, status, and provides remove actions. *(Polaris, Carbon, Atlassian agree: drop zone + accessible file input + per-file status list is the canonical pattern.)*
12
+
13
+ ---
14
+
15
+ ## Anatomy
16
+
17
+ ```
18
+ ┌─────────────────────────────────┐
19
+ │ Drop zone │
20
+ │ [ Cloud icon ] │
21
+ │ "Drag files here or" │
22
+ │ [ Browse files ] ← triggers │
23
+ │ <input type="file"> │
24
+ └─────────────────────────────────┘
25
+
26
+ File list (appears after selection):
27
+ ┌────────────────────────────────────────────────┐
28
+ │ 📄 report.pdf 245 KB [======== ] 80% [✕] │
29
+ │ 📄 photo.jpg 1.2 MB ✓ Done [✕] │
30
+ │ 📄 data.csv 88 KB ✗ Error [✕] │
31
+ └────────────────────────────────────────────────┘
32
+ ```
33
+
34
+ | Part | Required | Notes |
35
+ |------|----------|-------|
36
+ | Drop zone container | Yes | Dashed border; drag-over changes fill + border color |
37
+ | `<input type="file">` | Yes | MUST be accessible (not `display:none`) — keyboard fallback |
38
+ | Browse trigger button | Yes | Visually activates the file input; must be a `<button>` or `<label>` |
39
+ | File list | Yes (when files selected) | Per-file name, size, status, progress bar, remove button |
40
+ | Progress bar | Yes (during upload) | `role="progressbar"` + `aria-valuenow` per file or overall |
41
+ | Remove button | Yes | `aria-label="Remove [filename]"` |
42
+ | Error region | Yes (on error) | `aria-live="assertive"` for upload errors |
43
+
44
+ ---
45
+
46
+ ## Variants
47
+
48
+ | Variant | Description | Systems |
49
+ |---------|-------------|---------|
50
+ | Drop zone | Large dashed-border target area with drag-and-drop | Polaris, Carbon, Atlassian, Material 3 |
51
+ | Compact / inline | Small "Attach file" button only; no large drop area | Carbon (FileUploaderItem), Atlassian |
52
+ | Avatar/image uploader | Circular or rectangular crop zone for single image | Material 3, Polaris |
53
+ | Multi-file | `multiple` attr; list of uploaded files | All systems |
54
+ | Single-file | No `multiple`; replaces previous selection | Carbon, Polaris |
55
+
56
+ **Norm** (≥3/4 systems agree): drop zone + browse button + file list is the standard desktop pattern; progress bar per file during upload.
57
+ **Diverge**: Polaris auto-starts upload on drop; Carbon shows "Add files" button after initial selection to allow adding more; Material 3 defers to app logic.
58
+
59
+ ---
60
+
61
+ ## States
62
+
63
+ | State | Trigger | Visual | ARIA |
64
+ |-------|---------|--------|------|
65
+ | default | — | Dashed border, instructional text | — |
66
+ | drag-over | File dragged over zone | Filled background + solid border color change | `aria-dropeffect="copy"` (deprecated but still useful) |
67
+ | drag-invalid | Wrong file type dragged over | Error border color; tooltip/message | — |
68
+ | uploading | File being sent | Per-file progress bar animating | `aria-valuenow` on progressbar |
69
+ | upload-done | Transfer complete | Check icon; status text "Done" | — |
70
+ | upload-error | Transfer failed | Error icon; error message per file | `aria-live="assertive"` error region |
71
+ | disabled | `disabled` prop | 38% opacity; drag events ignored | `aria-disabled="true"` on zone |
72
+
73
+ ---
74
+
75
+ ## Sizing & Spacing
76
+
77
+ | Size | Drop Zone Min Height | Border Radius | Font |
78
+ |------|---------------------|---------------|------|
79
+ | sm | 80px | 4px | 13px |
80
+ | md (default) | 128px | 8px | 14px |
81
+ | lg | 200px | 12px | 16px |
82
+
83
+ **Norm**: Drop zone should be large enough that it is comfortably hittable — 128px minimum height for default. File list rows are 48–56px tall for accessible remove-button target size *(Carbon, Polaris)*.
84
+
85
+ Cross-link: `reference/surfaces.md` — minimum 44×44px touch targets for remove buttons.
86
+
87
+ ---
88
+
89
+ ## Typography
90
+
91
+ - Drop zone instruction: body-md, centered, secondary color
92
+ - "Browse files" link/button: body-md, primary link or button style
93
+ - File name in list: body-sm, truncated with ellipsis (max-width on container), full name in `title` attribute
94
+ - File size: caption-sm, secondary color
95
+ - Status text (Done / Error): caption-sm, success or error semantic color
96
+
97
+ Cross-link: `reference/typography.md` — truncation rules.
98
+
99
+ ---
100
+
101
+ ## Keyboard & Accessibility
102
+
103
+ > **WAI-ARIA role**: `button` (browse trigger); `progressbar` (upload progress); `status` or `log` (file list updates)
104
+ > **Required attributes**: `aria-label="Remove [filename]"` on remove buttons; `aria-valuenow` + `aria-valuemin` + `aria-valuemax` on progressbar; `aria-live="assertive"` on error region
105
+
106
+ ### Keyboard Contract
107
+
108
+ *Derived from native `<input type="file">` behavior and WAI-ARIA APG button pattern — W3C — 2024*
109
+
110
+ | Key | Action |
111
+ |-----|--------|
112
+ | Tab | Move focus to browse button / file input |
113
+ | Enter / Space | Activate browse button — opens native file picker dialog |
114
+ | Tab (in file list) | Move through file items and remove buttons |
115
+ | Enter / Space (on remove button) | Remove file from list |
116
+
117
+ Drag-and-drop is pointer-only; keyboard users MUST be able to complete the entire task via the file input alone.
118
+
119
+ ### Accessibility Rules
120
+
121
+ - `<input type="file">` MUST NOT use `display:none` or `visibility:hidden` — use `opacity:0` positioned absolutely with dimensions matching the trigger, OR keep a visible file input alongside the drop zone
122
+ - The browse trigger MUST be a `<button>` or `<label for="file-input">` so it is keyboard-focusable and activates the input
123
+ - Remove buttons MUST have `aria-label="Remove [filename]"` — an icon-only ✕ with no accessible name fails AT users
124
+ - Upload errors MUST be announced via `aria-live="assertive"` — do not rely solely on visual indicators
125
+ - Progress bars MUST keep `aria-valuenow` updated throughout upload
126
+ - `accept` attribute MUST match the visible allowed-types hint text so users are not surprised by rejection
127
+ - File list additions/removals should be announced via `aria-live="polite"` on the list container
128
+
129
+ Cross-link: `reference/accessibility.md` — aria-live regions, accessible file input patterns.
130
+
131
+ ---
132
+
133
+ ## Motion
134
+
135
+ | Transition | Duration | Easing | Notes |
136
+ |------------|----------|--------|-------|
137
+ | Drop zone drag-over | 100ms | ease-out | Background fill + border color |
138
+ | File list item enter | 200ms | ease-out | Slide-in from top or fade-in |
139
+ | File list item remove | 150ms | ease-in | Fade + collapse height |
140
+ | Progress bar fill | continuous | linear | Matches upload byte progress |
141
+ | Upload complete tick | 200ms | ease-out | Check icon draw animation |
142
+
143
+ **BAN**: Do not animate progress bar with CSS only at a fixed pace — progress MUST reflect actual upload percentage via `aria-valuenow`.
144
+
145
+ Cross-link: `reference/motion.md` — reduced-motion: skip slide/collapse animations; keep progress bar updates.
146
+
147
+ ---
148
+
149
+ ## Do / Don't
150
+
151
+ ### Do
152
+ - Keep `<input type="file">` accessible at all times (opacity:0 trick or visible) *(WAI-ARIA, Polaris, Carbon)*
153
+ - Provide `aria-label="Remove [filename]"` on every remove button *(Carbon, Atlassian)*
154
+ - Show file name, size, and status in the file list *(Polaris, Carbon, Atlassian)*
155
+ - Announce upload errors via `aria-live="assertive"` *(WCAG 2.1 §4.1.3 Status Messages)*
156
+
157
+ ### Don't
158
+ - Don't use `display:none` on the file input — keyboard and AT users cannot trigger the picker *(WCAG 2.1 §2.1.1)*
159
+ - Don't omit the `accept` hint text — users should know allowed types before selecting *(Polaris, Carbon)*
160
+ - Don't show only drag-drop UI with no browse button — drag is inaccessible to keyboard users *(Carbon, Atlassian)*
161
+ - Don't use a generic `aria-label="Remove"` on remove buttons — AT users cannot identify which file *(Carbon)*
162
+
163
+ ---
164
+
165
+ ## Anti-patterns Cross-links
166
+
167
+ | Anti-pattern | Entry |
168
+ |--------------|-------|
169
+ | BAN-08 | File input hidden with display:none — keyboard inaccessible — `reference/anti-patterns.md#ban-08` |
170
+ | BAN-13 | Icon-only action button without aria-label — `reference/anti-patterns.md#ban-13` |
171
+
172
+ ---
173
+
174
+ ## Benchmark Citations
175
+
176
+ | Claim | Sources |
177
+ |-------|---------|
178
+ | input type="file" must not be display:none | WCAG 2.1 §2.1.1, Carbon, Polaris accessibility guides |
179
+ | Remove button needs aria-label="Remove [filename]" | Carbon FileUploader, Atlassian, Polaris |
180
+ | Upload errors need aria-live="assertive" | WCAG 2.1 §4.1.3, Material 3 |
181
+ | Progress bar needs aria-valuenow updates | WAI-ARIA progressbar role spec |
182
+ | Drag-over state: background fill + border change | Polaris, Carbon, Atlassian drop zone specs |
183
+
184
+ Full system URLs: `connections/design-corpora.md`
185
+
186
+ ---
187
+
188
+ ## Grep Signatures
189
+
190
+ ```bash
191
+ # File input hidden with display:none (keyboard inaccessible)
192
+ grep -rn 'type="file"' src/ | grep -v 'opacity\|position.*absolute' | grep 'display.*none\|visibility.*hidden'
193
+
194
+ # Remove button missing aria-label (icon-only, no accessible name)
195
+ grep -rn 'remove.*button\|btn.*remove\|✕\|×' src/ | grep -v 'aria-label'
196
+
197
+ # Progress bar missing aria-valuenow
198
+ grep -rn 'role="progressbar"' src/ | grep -v 'aria-valuenow'
199
+
200
+ # Upload error region without aria-live
201
+ grep -rn 'upload.*error\|file.*error\|error.*upload' src/ | grep -v 'aria-live'
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Failing Example
207
+
208
+ ```html
209
+ <!-- BAD: drop zone with display:none on the actual <input> — keyboard and AT users can't trigger file picker -->
210
+ <div class="drop-zone" ondrop="handleDrop(event)" ondragover="handleDragOver(event)">
211
+ <p>Drag files here</p>
212
+ <input type="file" id="file-input" style="display:none" onchange="handleFiles(event)">
213
+ <button onclick="document.getElementById('file-input').click()">Browse</button>
214
+ </div>
215
+ ```
216
+
217
+ **Why it fails**: `display:none` removes the input from accessibility tree and tab order. The JavaScript `.click()` workaround does not work reliably with all AT. Keyboard users pressing Enter/Space on "Browse" may get inconsistent behavior across browsers. Screen reader users cannot discover or activate the file input directly.
218
+ **Grep detection**: `grep -rn 'type="file".*display.*none\|display.*none.*type="file"' src/`
219
+ **Fix**: Use `opacity:0; position:absolute; width:100%; height:100%` on the input (matching the browse button dimensions), or place a visible `<input type="file">` and style the button as a `<label for="file-input">` so clicking the label activates the input natively without JavaScript.
@@ -0,0 +1,217 @@
1
+ # List (Interactive & Display) — Benchmark Spec
2
+
3
+ **Harvested from**: Carbon, Polaris, Material 3, Mantine, WAI-ARIA APG, UUPM (app-interface, MIT)
4
+ **Wave**: 4 · **Category**: Navigation & Data
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A list component handles two distinct patterns: (1) a display list renders a series of items using semantic `<ul>/<ol>/<li>` HTML — no ARIA needed; (2) an interactive list (listbox) presents selectable options with keyboard navigation and selection state. Use a display list for content; use an interactive listbox when users choose one or more items from a set. *(Carbon, Polaris, Material 3 all define separate display and interactive list patterns)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ Display list: Interactive listbox:
18
+ <ul> <div role="listbox"
19
+ <li>Item one</li> aria-multiselectable="false"
20
+ <li>Item two</li> aria-label="Assignees">
21
+ <li>Item three</li> <div role="option"
22
+ </ul> aria-selected="true">Alice</div>
23
+ <div role="option"
24
+ aria-selected="false">Bob</div>
25
+ </div>
26
+ ```
27
+
28
+ | Part | Required | Notes |
29
+ |------|----------|-------|
30
+ | List container | Yes | `<ul>` / `<ol>` (display) or `role="listbox"` (interactive) |
31
+ | List item | Yes | `<li>` (display) or `role="option"` (interactive) |
32
+ | `aria-selected` | Interactive only | `true`/`false` on each `role="option"` |
33
+ | `aria-multiselectable` | Interactive only | `true` if multi-select; default `false` |
34
+ | `aria-label` / `aria-labelledby` | Interactive only | Describes the listbox purpose |
35
+ | Empty state | No | Min 200px height; illustration + message + optional CTA |
36
+ | Virtual scroll | Conditional | At > 100 items; TanStack Virtual or react-virtual |
37
+
38
+ ---
39
+
40
+ ## Variants
41
+
42
+ | Variant | Description | Systems |
43
+ |---------|-------------|---------|
44
+ | Unordered display | `<ul>` bullet list; purely semantic | All systems |
45
+ | Ordered display | `<ol>` numbered list; sequential content | All systems |
46
+ | Single-select listbox | One item selectable at a time | Carbon, Polaris, Material 3 |
47
+ | Multi-select listbox | Multiple items selectable (Shift+Click, Ctrl+Click) | Carbon, Material 3, Mantine |
48
+ | List / detail panel | Left-panel list + right detail pane | UUPM app-interface (MIT) |
49
+ | Recent-items list | Time-ordered recent items with timestamps | UUPM app-interface (MIT) |
50
+ | Virtualized | Windowed rendering for large datasets (> 100 items) | Carbon, Mantine |
51
+
52
+ **Norm** (≥4 systems agree): `role="listbox"` + `role="option"` for interactive; native `<ul>/<li>` for display.
53
+ **Diverge**: Material 3 calls the interactive variant "ListItem with selectable state"; Carbon uses "ContentSwitcher" for small sets and "MultiSelect" for large. Pattern semantics are identical.
54
+
55
+ ---
56
+
57
+ ## States
58
+
59
+ | State | Trigger | Visual | ARIA |
60
+ |-------|---------|--------|------|
61
+ | default | — | Items visible; none selected | `aria-selected="false"` on all options |
62
+ | option-hover | pointer over | 8% overlay | — |
63
+ | option-focus | keyboard focus | 2px focus-visible ring | managed via `tabindex` |
64
+ | option-selected | click or Enter/Space | Filled highlight; checkmark for multi-select | `aria-selected="true"` |
65
+ | option-disabled | disabled prop | 38% opacity; cursor: default | `aria-disabled="true"` |
66
+ | empty | no items | Empty state (illustration + text + CTA) | `aria-label` on empty container |
67
+ | loading | data fetch | Skeleton items | `aria-busy="true"` on listbox container |
68
+
69
+ ---
70
+
71
+ ## Sizing & Spacing
72
+
73
+ | Element | Value | Notes |
74
+ |---------|-------|-------|
75
+ | Item height | 40px (default) | 32px compact; 48px comfortable |
76
+ | Item padding H | 12–16px | Icon + 8px gap if icon present |
77
+ | Empty state min-height | 200px | Prevents visually collapsed empty container |
78
+ | Virtual viewport | ~400–600px | Clip height for virtualised scroll |
79
+ | List max-height | 400px default | Scroll within list container |
80
+
81
+ **Norm**: 40px item height (Carbon, Polaris, Mantine). Max-height + internal scroll for contained lists.
82
+
83
+ ---
84
+
85
+ ## Typography
86
+
87
+ - Display list: inherits parent body text; `<li>` marker via `list-style-type`
88
+ - Interactive option label: body-sm (13–14px), weight 400; selected weight 500
89
+ - Secondary text / metadata: label-xs (11–12px), `color: --text-subtle`
90
+ - Empty state heading: heading-sm, center-aligned
91
+ - Empty state body: body-sm, `color: --text-subtle`
92
+
93
+ Cross-link: `reference/typography.md` — body-sm, heading scale
94
+
95
+ ---
96
+
97
+ ## Keyboard & Accessibility
98
+
99
+ > **WAI-ARIA role**: `listbox` (interactive container), `option` (each item)
100
+ > **Required attributes**: `aria-selected` on each `role="option"`; `aria-label` or `aria-labelledby` on `role="listbox"`; `aria-multiselectable="true"` for multi-select
101
+
102
+ ### Keyboard Contract
103
+
104
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ — W3C — 2024*
105
+
106
+ | Key | Action |
107
+ |-----|--------|
108
+ | ArrowDown | Moves focus to next option (wraps to first) |
109
+ | ArrowUp | Moves focus to previous option (wraps to last) |
110
+ | Home | Moves focus to first option |
111
+ | End | Moves focus to last option |
112
+ | Enter / Space | Selects the focused option (single-select) |
113
+ | Shift+ArrowDown | Extends selection downward (multi-select) |
114
+ | Shift+ArrowUp | Extends selection upward (multi-select) |
115
+ | Ctrl+A | Selects all options (multi-select) |
116
+ | A–Z | Moves focus to next option starting with typed character |
117
+
118
+ ### Accessibility Rules
119
+
120
+ - Display lists use native `<ul>/<li>` — no ARIA roles needed; they are already accessible
121
+ - Interactive lists MUST use `role="listbox"` + `role="option"` — not `<ul>/<li>` with click handlers
122
+ - `aria-selected` MUST be present on every `role="option"` (either `true` or `false`)
123
+ - Multi-select listbox MUST declare `aria-multiselectable="true"` on the container
124
+ - Virtual scroll: all options in the virtualised window must have correct `aria-posinset` and `aria-setsize` attributes
125
+ - Empty state container MUST have `aria-label` or `aria-live` so AT announces the empty state
126
+
127
+ Cross-link: `reference/accessibility.md` — listbox pattern, virtual list accessibility
128
+
129
+ ---
130
+
131
+ ## Motion
132
+
133
+ | Transition | Duration | Easing | Notes |
134
+ |------------|----------|--------|-------|
135
+ | Option selection highlight | 100ms | ease-out | Background color only |
136
+ | Skeleton item shimmer | 1500ms | linear loop | Loading placeholder |
137
+ | Empty state entry | 150ms | ease-out | Fade in |
138
+
139
+ **BAN**: Do not animate item reordering unless using a deliberate drag-and-drop library — unsolicited reordering causes disorientation.
140
+
141
+ Cross-link: `reference/motion.md` — skeleton shimmer, list animations
142
+
143
+ ---
144
+
145
+ ## Do / Don't
146
+
147
+ ### Do
148
+ - Use native `<ul>/<ol>/<li>` for display-only lists — no ARIA needed *(WAI-ARIA APG)*
149
+ - Use `role="listbox"` + `role="option"` for selectable lists *(WAI-ARIA APG, Carbon, Polaris)*
150
+ - Virtualise at > 100 items to prevent DOM bloat *(Carbon, Mantine)*
151
+ - Provide a meaningful empty state with a CTA when the list can be populated *(Polaris, Material 3)*
152
+
153
+ ### Don't
154
+ - Don't use `<div onClick>` list items without `role="option"` — keyboard-inaccessible *(WCAG 2.1.1)*
155
+ - Don't omit `aria-selected` on options — AT cannot determine what is selected *(WAI-ARIA APG)*
156
+ - Don't use `<ul>/<li>` with `role="option"` — mixing native list semantics and listbox ARIA creates conflicts *(WAI-ARIA)*
157
+ - Don't load all items at once when count > 100 — renders slowly and wastes memory *(Carbon, Mantine)*
158
+
159
+ ---
160
+
161
+ ## Anti-patterns Cross-links
162
+
163
+ | Anti-pattern | Entry |
164
+ |--------------|-------|
165
+ | BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
166
+
167
+ ---
168
+
169
+ ## Benchmark Citations
170
+
171
+ | Claim | Sources |
172
+ |-------|---------|
173
+ | role="listbox" + role="option" for interactive lists | WAI-ARIA APG listbox pattern |
174
+ | aria-selected required on every option | WAI-ARIA APG, Carbon, Polaris |
175
+ | aria-multiselectable="true" for multi-select | WAI-ARIA APG |
176
+ | Virtualise at > 100 items | Carbon, Mantine (TanStack Virtual) |
177
+ | 200px min empty state height | Carbon, Polaris HIG |
178
+
179
+ Full system URLs: `connections/design-corpora.md`
180
+
181
+ ---
182
+
183
+ ## Grep Signatures
184
+
185
+ ```bash
186
+ # Interactive list items using <div> without role="option"
187
+ grep -rn '<div' src/ | grep -i 'list.*item\|list-item\|listitem' | grep 'onClick\|on:click' | grep -v 'role='
188
+
189
+ # Missing aria-selected on option elements
190
+ grep -rn 'role="option"' src/ | grep -v 'aria-selected'
191
+
192
+ # Listbox missing aria-label
193
+ grep -rn 'role="listbox"' src/ | grep -v 'aria-label\|aria-labelledby'
194
+
195
+ # <ul>/<li> used with role="option" (semantics conflict)
196
+ grep -rn 'role="option"' src/ | grep '<li'
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Failing Example
202
+
203
+ ```html
204
+ <!-- BAD: interactive list items using <div onClick> with no keyboard support -->
205
+ <div class="user-list"> <!-- no role="listbox" -->
206
+ <div class="list-item selected" onclick="selectUser('alice')">
207
+ Alice Chen
208
+ </div>
209
+ <div class="list-item" onclick="selectUser('bob')">
210
+ Bob Tanaka
211
+ </div>
212
+ </div>
213
+ ```
214
+
215
+ **Why it fails**: No `role="listbox"` on container; no `role="option"` on items; no `aria-selected`; items not keyboard-focusable (no `tabindex`); arrow-key navigation does nothing; screen readers see two unlabelled `<div>` elements.
216
+ **Grep detection**: `grep -rn '<div.*onClick\|<div.*on:click' src/ | grep -i 'list.*item\|listitem'`
217
+ **Fix**: Use `<div role="listbox" aria-label="Users">` with `<div role="option" tabindex="-1" aria-selected="false">` items; implement roving `tabindex` and arrow-key handlers.
@@ -0,0 +1,212 @@
1
+ # Menu (Dropdown / Context Menu) — Benchmark Spec
2
+
3
+ **Harvested from**: Radix UI, WAI-ARIA APG, Carbon, Atlassian, Material 3, Polaris
4
+ **Wave**: 4 · **Category**: Navigation
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A menu presents a list of actions or options in a temporary overlay anchored to a trigger element. It differs from a Select/Combobox (which chooses a value) — a menu executes commands or navigates. Use a Dropdown Menu for trigger-button scenarios; use a Context Menu for right-click/long-press on an element. *(Radix DropdownMenu, Carbon OverflowMenu, Atlassian DropdownMenu, WAI-ARIA APG Menu agree: menus are action lists, not value selectors)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ [ Trigger Button ▾ ]
18
+ ┌─────────────────────┐
19
+ │ ✓ Menu Item │ role="menuitemcheckbox"
20
+ │ ── Separator ── │ role="separator"
21
+ │ › Sub-menu Item │ role="menuitem" aria-haspopup="menu"
22
+ │ Edit │ role="menuitem"
23
+ │ Delete │ role="menuitem"
24
+ └─────────────────────┘
25
+ ```
26
+
27
+ | Part | Required | Notes |
28
+ |------|----------|-------|
29
+ | Trigger | Yes | `<button>` with `aria-haspopup="menu"` + `aria-expanded` |
30
+ | Menu container | Yes | `role="menu"` + `aria-labelledby` pointing to trigger |
31
+ | Menu item | Yes | `role="menuitem"` on each action |
32
+ | Separator | No | `role="separator"` — groups related items |
33
+ | Checkbox item | No | `role="menuitemcheckbox"` + `aria-checked` |
34
+ | Radio item | No | `role="menuitemradio"` + `aria-checked`; group in `role="group"` |
35
+ | Sub-menu trigger | No | `role="menuitem"` + `aria-haspopup="menu"` + `aria-expanded` |
36
+ | Focus indicator | Yes | 2px focus-visible ring on keyboard-active item |
37
+
38
+ ---
39
+
40
+ ## Variants
41
+
42
+ | Variant | Description | Systems |
43
+ |---------|-------------|---------|
44
+ | Dropdown menu | Opens from a button trigger; anchored below/above | Radix, Carbon, Atlassian, Polaris |
45
+ | Context menu | Opens at pointer position on right-click or long-press | Radix, Material 3, Carbon |
46
+ | Overflow menu | Icon-only trigger (⋯ or ⋮); common in dense UIs | Carbon OverflowMenu, Atlassian |
47
+ | Sub-menu | Cascading child menu opening on ArrowRight | Radix, Atlassian, Carbon |
48
+ | Checkbox/radio menu | Items with persistent checked state | Radix, Carbon, Material 3 |
49
+
50
+ **Norm** (≥4 systems agree): flat action list with separator groups; avoid > 2 nesting levels.
51
+ **Diverge**: Polaris uses ActionList as a shared primitive for both menus and select options; Carbon splits OverflowMenu from ContextMenu as separate components.
52
+
53
+ ---
54
+
55
+ ## States
56
+
57
+ | State | Trigger | Visual | ARIA |
58
+ |-------|---------|--------|------|
59
+ | closed | — | Trigger visible; overlay hidden | `aria-expanded="false"` on trigger |
60
+ | open | Click trigger | Overlay visible; first item focused | `aria-expanded="true"` on trigger |
61
+ | item-hover / focus | Arrow keys or pointer | Item highlight (8% overlay) | `tabindex="-1"` managed via roving tabindex |
62
+ | item-disabled | `disabled` prop | 38% opacity, cursor: default | `aria-disabled="true"` on item |
63
+ | checked | Toggle menuitemcheckbox | Checkmark icon visible | `aria-checked="true"` |
64
+ | submenu-open | ArrowRight on parent | Child menu visible | `aria-expanded="true"` on parent item |
65
+
66
+ ---
67
+
68
+ ## Sizing & Spacing
69
+
70
+ | Element | Value | Notes |
71
+ |---------|-------|-------|
72
+ | Min menu width | 160px | Prevents awkward narrow menus |
73
+ | Max menu width | 320px | Truncate labels with ellipsis beyond |
74
+ | Item height | 36px (md) | 32px compact, 40px comfortable |
75
+ | Item padding H | 12px | Icon if present: 16px left + 8px gap |
76
+ | Separator height | 1px + 4px V margin | Divider line |
77
+ | Icon size | 16px | Left-aligned, consistent with label baseline |
78
+
79
+ **Norm**: 36px item height (Radix default, Carbon, Atlassian). Min-width 160px prevents single-word menus.
80
+
81
+ ---
82
+
83
+ ## Typography
84
+
85
+ - Item label: body-sm (13–14px), weight 400 — not bold; action labels read as text, not controls
86
+ - Destructive items: same weight, color token `--color-text-danger`
87
+ - Keyboard shortcut hints: body-xs (11–12px), muted color, right-aligned, `aria-hidden="true"`
88
+ - Truncate item labels with `text-overflow: ellipsis`; never wrap item text
89
+
90
+ Cross-link: `reference/typography.md` — body-sm scale
91
+
92
+ ---
93
+
94
+ ## Keyboard & Accessibility
95
+
96
+ > **WAI-ARIA role**: `menu` (container), `menuitem` / `menuitemcheckbox` / `menuitemradio` (items)
97
+ > **Required attributes**: `aria-haspopup="menu"` + `aria-expanded` on trigger; `aria-labelledby` on `role="menu"`
98
+
99
+ ### Keyboard Contract
100
+
101
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/ — W3C — 2024*
102
+
103
+ | Key | Action |
104
+ |-----|--------|
105
+ | Enter / Space | Opens menu from trigger; activates focused item |
106
+ | ArrowDown | Moves focus to next item (wraps to first) |
107
+ | ArrowUp | Moves focus to previous item (wraps to last) |
108
+ | Escape | Closes menu; returns focus to trigger |
109
+ | Tab | Closes menu; moves focus to next focusable element (does not cycle through items) |
110
+ | Home | Moves focus to first item |
111
+ | End | Moves focus to last item |
112
+ | A–Z / a–z | Moves focus to next item starting with that character |
113
+ | ArrowRight | Opens sub-menu; moves focus to first item of sub-menu |
114
+ | ArrowLeft | Closes sub-menu; returns focus to parent item |
115
+
116
+ ### Accessibility Rules
117
+
118
+ - Menu MUST open on click only — never on hover for primary open (hover may preview sub-menus)
119
+ - All items MUST be reachable by keyboard; no mouse-only items
120
+ - Focus returns to the trigger element when the menu closes
121
+ - Keyboard shortcut labels (e.g. "⌘K") are `aria-hidden="true"` — the shortcut must be registered separately
122
+ - `role="separator"` dividers are not focusable
123
+ - Disabled items use `aria-disabled="true"` (keep focusable so AT users know the option exists)
124
+
125
+ Cross-link: `reference/accessibility.md` — focus management, roving tabindex
126
+
127
+ ---
128
+
129
+ ## Motion
130
+
131
+ | Transition | Duration | Easing | Notes |
132
+ |------------|----------|--------|-------|
133
+ | Menu enter | 120ms | ease-out | Scale 0.95→1 + opacity 0→1 from anchor point |
134
+ | Menu exit | 80ms | ease-in | Opacity 1→0; skip scale-down for speed |
135
+ | Item highlight | 80ms | ease-out | Background color transition only |
136
+ | Sub-menu enter | 120ms | ease-out | Same as menu enter |
137
+
138
+ **BAN**: `transition: all` on menu items — triggers layout thrash on width changes.
139
+
140
+ Cross-link: `reference/motion.md` — overlay entry pattern, BAN-04
141
+
142
+ ---
143
+
144
+ ## Do / Don't
145
+
146
+ ### Do
147
+ - Use `role="menu"` + `role="menuitem"` for all action menus *(WAI-ARIA APG)*
148
+ - Group related items with `role="separator"` — keep groups ≤ 7 items *(Carbon, Atlassian)*
149
+ - Return focus to the trigger on close *(WAI-ARIA APG)*
150
+ - Use `role="menuitemcheckbox"` for persistent toggle states *(Radix, Material 3)*
151
+
152
+ ### Don't
153
+ - Don't open the menu on hover as the primary interaction — keyboard users can't discover hover *(WCAG 1.3.3)*
154
+ - Don't exceed 2 levels of sub-menus — deeply nested menus are cognitively expensive *(Atlassian, Carbon)*
155
+ - Don't put form controls (inputs, sliders) inside a menu — use a Popover instead *(WAI-ARIA APG)*
156
+ - Don't use `<div>` items without `role="menuitem"` — invisible to screen readers *(WAI-ARIA)*
157
+
158
+ ---
159
+
160
+ ## Anti-patterns Cross-links
161
+
162
+ | Anti-pattern | Entry |
163
+ |--------------|-------|
164
+ | BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
165
+
166
+ ---
167
+
168
+ ## Benchmark Citations
169
+
170
+ | Claim | Sources |
171
+ |-------|---------|
172
+ | role="menu" + role="menuitem" contract | WAI-ARIA APG Menu Button pattern |
173
+ | Click-only open (not hover) | WAI-ARIA APG, WCAG 1.3.3, Carbon |
174
+ | ArrowRight/Left for sub-menu navigation | WAI-ARIA APG, Radix DropdownMenu |
175
+ | Focus returns to trigger on close | WAI-ARIA APG, Radix |
176
+ | 36px item height | Radix default, Carbon OverflowMenu, Atlassian |
177
+
178
+ Full system URLs: `connections/design-corpora.md`
179
+
180
+ ---
181
+
182
+ ## Grep Signatures
183
+
184
+ ```bash
185
+ # Menu container missing role="menu"
186
+ grep -rn 'dropdown\|context-menu\|overflow-menu' src/ | grep -v 'role="menu"'
187
+
188
+ # Items using <div> without role="menuitem"
189
+ grep -rn '<div' src/ | grep -i 'menu-item\|menuitem\|menu__item' | grep -v 'role='
190
+
191
+ # Trigger missing aria-haspopup
192
+ grep -rn 'aria-expanded' src/ | grep -i 'menu\|dropdown' | grep -v 'aria-haspopup'
193
+
194
+ # Missing aria-labelledby on menu container
195
+ grep -rn 'role="menu"' src/ | grep -v 'aria-labelledby\|aria-label'
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Failing Example
201
+
202
+ ```html
203
+ <!-- BAD: div list with click handlers but no ARIA roles -->
204
+ <div class="dropdown-menu">
205
+ <div class="dropdown-item" onclick="handleEdit()">Edit</div>
206
+ <div class="dropdown-item" onclick="handleDelete()">Delete</div>
207
+ </div>
208
+ ```
209
+
210
+ **Why it fails**: No `role="menu"` or `role="menuitem"` — screen readers cannot announce this as a menu; items are not keyboard-navigable; no arrow-key navigation; trigger lacks `aria-haspopup` and `aria-expanded`.
211
+ **Grep detection**: `grep -rn '<div.*onclick\|<div.*onClick' src/ | grep -i 'menu\|dropdown'`
212
+ **Fix**: Use `<ul role="menu">` with `<li role="menuitem" tabindex="-1">` items, or a headless menu primitive (Radix DropdownMenu, Downshift).