@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.
- package/.claude-plugin/marketplace.json +9 -5
- package/.claude-plugin/plugin.json +19 -5
- package/CHANGELOG.md +122 -0
- package/README.md +41 -0
- package/SKILL.md +4 -1
- package/agents/component-benchmark-harvester.md +112 -0
- package/agents/component-benchmark-synthesizer.md +88 -0
- package/agents/design-auditor.md +60 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +61 -0
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/connections/design-corpora.md +158 -0
- package/package.json +13 -3
- package/reference/components/README.md +94 -0
- package/reference/components/TEMPLATE.md +184 -0
- package/reference/components/accordion.md +217 -0
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -0
- package/reference/components/button.md +195 -0
- package/reference/components/card.md +200 -0
- package/reference/components/checkbox.md +207 -0
- package/reference/components/chip.md +209 -0
- package/reference/components/command-palette.md +228 -0
- package/reference/components/date-picker.md +227 -0
- package/reference/components/drawer.md +201 -0
- package/reference/components/file-upload.md +219 -0
- package/reference/components/input.md +208 -0
- package/reference/components/label.md +200 -0
- package/reference/components/link.md +193 -0
- package/reference/components/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/modal-dialog.md +210 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/popover.md +197 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/radio.md +203 -0
- package/reference/components/rich-text-editor.md +226 -0
- package/reference/components/select-combobox.md +219 -0
- package/reference/components/sidebar.md +211 -0
- package/reference/components/skeleton.md +197 -0
- package/reference/components/slider.md +208 -0
- package/reference/components/stepper.md +220 -0
- package/reference/components/switch.md +194 -0
- package/reference/components/table.md +229 -0
- package/reference/components/tabs.md +213 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tooltip.md +201 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/image-optimization.md +582 -0
- package/reference/motion-advanced.md +754 -0
- package/reference/motion-easings.md +381 -0
- package/reference/motion-interpolate.md +282 -0
- package/reference/motion-spring.md +234 -0
- package/reference/motion-transition-taxonomy.md +155 -0
- package/reference/motion.md +20 -0
- package/reference/output-contracts/motion-map.schema.json +135 -0
- package/reference/registry.json +285 -0
- package/reference/registry.schema.json +6 -1
- package/reference/variable-fonts-loading.md +532 -0
- package/scripts/lib/easings.cjs +280 -0
- package/scripts/lib/parse-contract.cjs +220 -0
- package/scripts/lib/spring.cjs +160 -0
- package/scripts/tests/test-motion-provenance.sh +64 -0
- package/skills/benchmark/SKILL.md +105 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Link — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Carbon, Polaris, Primer (GitHub), Fluent 2, WAI-ARIA APG, Material 3, Mantine, Atlassian
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A link navigates the user to a new resource — another page, section, or external URL. It is the semantic counterpart to Button: if clicking takes the user somewhere, it is a `<a href>` link; if it triggers an action in the current context, it is a button. Never reverse these roles. *(Carbon, Primer, WAI-ARIA APG all enforce this boundary)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Inline: Read the [full documentation] for details.
|
|
18
|
+
└── <a href="..."> — inline, within body text
|
|
19
|
+
|
|
20
|
+
Standalone: [→ View report]
|
|
21
|
+
└── <a href="..."> — block-level, not inline in prose
|
|
22
|
+
|
|
23
|
+
External: [Open dashboard ↗]
|
|
24
|
+
└── <a href="..." target="_blank" rel="noopener noreferrer">
|
|
25
|
+
+ aria-label="Open dashboard (opens in new tab)"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Part | Required | Notes |
|
|
29
|
+
|------|----------|-------|
|
|
30
|
+
| `<a href>` | Yes | Native element; href MUST be a real URL or `#anchor` |
|
|
31
|
+
| Visible label | Yes | Descriptive of destination — not "click here" or "read more" |
|
|
32
|
+
| Underline | Conditional | Required for inline links in body text; optional for standalone/nav |
|
|
33
|
+
| Visited state | No | Encouraged for inline links in long-form content |
|
|
34
|
+
| External icon | Conditional | Required when `target="_blank"`; 12–14px, inline-aligned |
|
|
35
|
+
| `rel="noopener noreferrer"` | Yes (external) | Security — prevents opener access |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Variants
|
|
40
|
+
|
|
41
|
+
| Variant | Description | Systems |
|
|
42
|
+
|---------|-------------|---------|
|
|
43
|
+
| Inline | Within prose; always underlined | All |
|
|
44
|
+
| Standalone | Block-level CTA; underline optional | Carbon, Polaris, Primer |
|
|
45
|
+
| Nav / breadcrumb | In navigation contexts; no underline | All |
|
|
46
|
+
| External | `target="_blank"` with external icon | All |
|
|
47
|
+
| Destructive | Rare; red colour for delete-via-link patterns | Polaris (critical link) |
|
|
48
|
+
| Disabled | `aria-disabled="true"` + `tabindex="-1"` | Carbon, Primer |
|
|
49
|
+
|
|
50
|
+
**Norm** (≥6/18): inline links in body text MUST be underlined — colour alone fails WCAG 1.4.1.
|
|
51
|
+
**Diverge**: visited state — Primer and Carbon use it for documentation; most SaaS systems omit it.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## States
|
|
56
|
+
|
|
57
|
+
| State | Visual | ARIA / HTML |
|
|
58
|
+
|-------|--------|-------------|
|
|
59
|
+
| default | Underline + colour | `href` present |
|
|
60
|
+
| hover | Colour shift (10% darker/lighter) | — |
|
|
61
|
+
| focus | 2px focus-visible ring | — |
|
|
62
|
+
| active / pressed | Colour darkens + subtle scale 0.98 | — |
|
|
63
|
+
| visited | Distinct colour (purple conventional) | `:visited` pseudo-class |
|
|
64
|
+
| disabled | 38% opacity; pointer-events: none | `aria-disabled="true"` + `tabindex="-1"` |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Sizing & Spacing
|
|
69
|
+
|
|
70
|
+
- Links inherit parent font-size and line-height — do not override
|
|
71
|
+
- Standalone link min-height: 44px via padding for touch targets
|
|
72
|
+
- Icon size (external/leading): 12–14px; `vertical-align: middle`; 4px gap from text
|
|
73
|
+
|
|
74
|
+
Cross-link: `reference/surfaces.md` — hit-area pattern for standalone links
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Typography
|
|
79
|
+
|
|
80
|
+
- Inline links: same weight as surrounding text (400); underline distinguishes them
|
|
81
|
+
- Standalone links: 400–500 weight; may have leading icon or trailing arrow
|
|
82
|
+
- Never use ALL CAPS for links — reduces readability and implies different semantics
|
|
83
|
+
- Truncate with ellipsis + `title` attribute only when space is genuinely constrained
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Keyboard & Accessibility
|
|
88
|
+
|
|
89
|
+
> **WAI-ARIA role**: `link` (implicit on `<a href>`)
|
|
90
|
+
> **Required attributes**: `href` — without it, the element is not a link and has no keyboard access
|
|
91
|
+
|
|
92
|
+
### Keyboard Contract
|
|
93
|
+
|
|
94
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/link/ — W3C — 2024*
|
|
95
|
+
|
|
96
|
+
| Key | Action |
|
|
97
|
+
|-----|--------|
|
|
98
|
+
| Enter | Activates the link and navigates to its destination |
|
|
99
|
+
| Tab | Moves focus to the next focusable element |
|
|
100
|
+
| Shift+Tab | Moves focus to the previous focusable element |
|
|
101
|
+
|
|
102
|
+
### Accessibility Rules
|
|
103
|
+
|
|
104
|
+
- Link text MUST describe the destination — "click here" and "read more" fail 2.4.6 (descriptive labels)
|
|
105
|
+
- External links opening in new tab MUST disclose this: append "(opens in new tab)" to `aria-label`, or use a visually-hidden span
|
|
106
|
+
- `target="_blank"` MUST always be paired with `rel="noopener noreferrer"` (security + performance)
|
|
107
|
+
- Disabled links: `aria-disabled="true"` + `tabindex="-1"` — never `href=""` or `href="#"`
|
|
108
|
+
- Inline links in body text MUST be underlined — colour alone is insufficient for WCAG 1.4.1 (non-text contrast)
|
|
109
|
+
- Icon-only links (e.g., social icons) MUST have `aria-label` describing the destination
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Motion
|
|
114
|
+
|
|
115
|
+
| Transition | Duration | Easing | Notes |
|
|
116
|
+
|------------|----------|--------|-------|
|
|
117
|
+
| Colour on hover | 100ms | ease | Subtle; avoid opacity changes (readability) |
|
|
118
|
+
| Underline decoration | 80ms | ease | Underline grow or fade for standalone variants |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Do / Don't
|
|
123
|
+
|
|
124
|
+
### Do
|
|
125
|
+
- Use descriptive link text: "View account settings" not "click here" *(Polaris, Carbon, WAI-ARIA APG)*
|
|
126
|
+
- Underline inline links in body text *(Carbon, Polaris, WAI-ARIA APG — WCAG 1.4.1)*
|
|
127
|
+
- Add `rel="noopener noreferrer"` to all `target="_blank"` links *(Primer, Carbon, Fluent 2)*
|
|
128
|
+
- Disclose new-tab behavior in `aria-label` or visually-hidden text *(WAI-ARIA APG, Primer)*
|
|
129
|
+
|
|
130
|
+
### Don't
|
|
131
|
+
- Don't use `<a>` without `href` — it's not a link, not keyboard-accessible, and will confuse screen readers *(WAI-ARIA APG)*
|
|
132
|
+
- Don't use `<button>` when the action is navigation *(Carbon, Primer)*
|
|
133
|
+
- Don't rely on colour alone to distinguish links from surrounding text *(WCAG 1.4.1)*
|
|
134
|
+
- Don't open links in new tabs unexpectedly without disclosure *(Polaris, Primer, WCAG 3.2.5)*
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Anti-patterns Cross-links
|
|
139
|
+
|
|
140
|
+
| Anti-pattern | Entry |
|
|
141
|
+
|--------------|-------|
|
|
142
|
+
| Anchor without href | `reference/anti-patterns.md` |
|
|
143
|
+
| Non-descriptive link text | `reference/anti-patterns.md` |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Benchmark Citations
|
|
148
|
+
|
|
149
|
+
| Claim | Sources |
|
|
150
|
+
|-------|---------|
|
|
151
|
+
| Enter activates link | WAI-ARIA APG §3.3 |
|
|
152
|
+
| Underline required for inline links | Carbon, Polaris, WCAG 1.4.1 |
|
|
153
|
+
| rel="noopener noreferrer" for _blank | Primer, Carbon, Fluent 2 |
|
|
154
|
+
| "click here" is anti-pattern | Polaris, Carbon, WAI-ARIA APG |
|
|
155
|
+
|
|
156
|
+
Full system URLs: `connections/design-corpora.md`
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Grep Signatures
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# <a> without href (not a link — no keyboard access)
|
|
164
|
+
grep -rn '<a ' src/ | grep -v 'href='
|
|
165
|
+
|
|
166
|
+
# target="_blank" without rel="noopener noreferrer"
|
|
167
|
+
grep -rn 'target="_blank"' src/ | grep -v 'rel=.*noopener'
|
|
168
|
+
|
|
169
|
+
# Non-descriptive link text
|
|
170
|
+
grep -rn '>click here\|>read more\|>learn more\|>here<' src/ | grep -i '<a'
|
|
171
|
+
|
|
172
|
+
# Colour-only link distinction (no text-decoration)
|
|
173
|
+
grep -rn 'text-decoration:\s*none' src/ | grep -i 'link\|<a'
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Failing Example
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<!-- BAD: non-descriptive link text + missing rel on external link -->
|
|
182
|
+
<a href="https://docs.example.com" target="_blank">Click here</a>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Why it fails**: "Click here" gives no destination context (fails WCAG 2.4.6). Missing `rel="noopener noreferrer"` is a security vulnerability. No disclosure of new-tab behavior.
|
|
186
|
+
**Grep detection**: `grep -rn '>click here\|>here<\|>read more' src/`
|
|
187
|
+
**Fix**:
|
|
188
|
+
```html
|
|
189
|
+
<a href="https://docs.example.com" target="_blank" rel="noopener noreferrer"
|
|
190
|
+
aria-label="View documentation (opens in new tab)">
|
|
191
|
+
View documentation <span aria-hidden="true">↗</span>
|
|
192
|
+
</a>
|
|
193
|
+
```
|
|
@@ -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).
|