@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,226 @@
|
|
|
1
|
+
# Rich-Text Editor — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Tiptap/ProseMirror, Lexical (Meta), Slate.js, Atlassian Fabric Editor
|
|
4
|
+
**Wave**: 5 · **Category**: Advanced
|
|
5
|
+
**Spec file**: `reference/components/rich-text-editor.md`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
A Rich-Text Editor (RTE) provides WYSIWYG content authoring with inline formatting (bold, italic, links), block-level elements (headings, lists, blockquotes), and optional advanced features (mentions, embeds, tables). It is appropriate for long-form content where plain `<textarea>` is insufficient and a full CMS-style document editor is overkill. *(Tiptap, Lexical, Atlassian agree: contenteditable + ProseMirror/Lexical model + explicit toolbar is the production-grade RTE pattern.)*
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Anatomy
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌── role="toolbar" aria-label="Text formatting" ──────────────────┐
|
|
19
|
+
│ [B] [I] [U] [S] │ [H1][H2] │ [• List][1. List] │ [Link][Image] │
|
|
20
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
21
|
+
┌── role="textbox" aria-multiline="true" aria-label="Post body" ──┐
|
|
22
|
+
│ │
|
|
23
|
+
│ Start typing here… (placeholder via CSS ::before) │
|
|
24
|
+
│ │
|
|
25
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Part | Required | Notes |
|
|
29
|
+
|------|----------|-------|
|
|
30
|
+
| Editable area | Yes | `contenteditable="true"` + `role="textbox"` + `aria-multiline="true"` |
|
|
31
|
+
| Toolbar | Yes (for formatting) | `role="toolbar"` + `aria-label`; groups via `role="group"` |
|
|
32
|
+
| Toolbar buttons | Yes | `role="button"`; toggle buttons add `aria-pressed` |
|
|
33
|
+
| Placeholder | No | CSS `[data-placeholder]::before` — NOT HTML attribute |
|
|
34
|
+
| Mention list | No | `role="listbox"` floating suggestion list triggered by `@` |
|
|
35
|
+
| Character count | No | `aria-live="polite"` updated region |
|
|
36
|
+
| Read-only overlay | No | `contenteditable="false"` + `aria-readonly="true"` |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Variants
|
|
41
|
+
|
|
42
|
+
| Variant | Description | Systems |
|
|
43
|
+
|---------|-------------|---------|
|
|
44
|
+
| Minimal | Bold/italic/underline + link only | Tiptap (StarterKit minimal), Atlassian (inline comment) |
|
|
45
|
+
| Standard | Full paragraph formatting + lists + links | Tiptap, Lexical, Slate |
|
|
46
|
+
| Document | Full editor with headings, tables, embeds, mentions | Atlassian Fabric Editor, Tiptap (full), Lexical (full) |
|
|
47
|
+
| Read-only | Rendered content; no editing | All systems |
|
|
48
|
+
| Bubble toolbar | Formatting toolbar appears on text selection (tooltip style) | Tiptap, Atlassian inline editor |
|
|
49
|
+
|
|
50
|
+
**Norm** (≥3/4 systems agree): toolbar at top or on selection; contenteditable with explicit role="textbox"; keyboard shortcuts for core formatting.
|
|
51
|
+
**Diverge**: Atlassian uses a floating toolbar on selection; Tiptap supports both fixed and bubble modes; Lexical uses a plugin architecture; Slate treats everything as React tree nodes.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## States
|
|
56
|
+
|
|
57
|
+
| State | Trigger | Visual | ARIA |
|
|
58
|
+
|-------|---------|--------|------|
|
|
59
|
+
| default | — | Cursor; toolbar buttons at rest | — |
|
|
60
|
+
| focused | Click or Tab into editable area | Focus ring on editable container | — |
|
|
61
|
+
| toolbar button active | Text with formatting selected | `aria-pressed="true"` on toggle button | `aria-pressed="true"` |
|
|
62
|
+
| placeholder | No content in editable area | Placeholder text via CSS ::before | — |
|
|
63
|
+
| read-only | `readOnly` prop | No cursor change; grayed toolbar | `aria-readonly="true"`, `contenteditable="false"` |
|
|
64
|
+
| disabled | `disabled` prop | 38% opacity; no interaction | `aria-disabled="true"` |
|
|
65
|
+
| mention-open | `@` typed | Floating listbox appears | `role="listbox"` visible |
|
|
66
|
+
| error | Validation failure | Red border; error message below | `aria-describedby` → error message |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Sizing & Spacing
|
|
71
|
+
|
|
72
|
+
| Size | Min Height | Toolbar Height | Font |
|
|
73
|
+
|------|-----------|----------------|------|
|
|
74
|
+
| sm | 80px | 32px | 13px |
|
|
75
|
+
| md (default) | 160px | 40px | 14px |
|
|
76
|
+
| lg | 320px | 48px | 16px |
|
|
77
|
+
|
|
78
|
+
**Norm**: Editor area grows with content (auto-height); enforce max-height with overflow scroll if needed. Toolbar buttons follow button-sm/md sizing with 8px gaps between groups *(Atlassian, Tiptap)*.
|
|
79
|
+
|
|
80
|
+
Cross-link: `reference/surfaces.md` — toolbar button hit targets.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Typography
|
|
85
|
+
|
|
86
|
+
- Editor content: body-md, line-height 1.6 for readable long-form text
|
|
87
|
+
- Heading 1: 2em; Heading 2: 1.5em; Heading 3: 1.25em — relative to editor base font
|
|
88
|
+
- Code blocks: monospace, 0.9em, background token surface-code
|
|
89
|
+
- Placeholder text: same size/font as body, secondary color via CSS
|
|
90
|
+
|
|
91
|
+
Cross-link: `reference/typography.md` — heading scale, code font stack.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Keyboard & Accessibility
|
|
96
|
+
|
|
97
|
+
> **WAI-ARIA role**: `textbox` (editable area), `toolbar` (toolbar container), `group` (toolbar sections), `button` (toolbar actions), `listbox` (mention suggestions)
|
|
98
|
+
> **Required attributes**: `role="textbox"` + `aria-multiline="true"` + `aria-label` or `aria-labelledby` on editable area; `aria-pressed` on toggle toolbar buttons; `role="toolbar"` + `aria-label` on toolbar
|
|
99
|
+
|
|
100
|
+
### Keyboard Contract
|
|
101
|
+
|
|
102
|
+
*Derived from WAI-ARIA APG toolbar pattern — https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/ — W3C — 2024, and standard contenteditable browser behavior*
|
|
103
|
+
|
|
104
|
+
| Key | Action |
|
|
105
|
+
|-----|--------|
|
|
106
|
+
| Tab | Move focus into the editable area from outside; move out of editor |
|
|
107
|
+
| Ctrl/Cmd + B | Toggle bold on selected text |
|
|
108
|
+
| Ctrl/Cmd + I | Toggle italic on selected text |
|
|
109
|
+
| Ctrl/Cmd + U | Toggle underline on selected text |
|
|
110
|
+
| Ctrl/Cmd + Z | Undo last action |
|
|
111
|
+
| Ctrl/Cmd + Shift + Z | Redo |
|
|
112
|
+
| Ctrl/Cmd + K | Insert or edit link |
|
|
113
|
+
| @ (in editor) | Trigger mention suggestion list |
|
|
114
|
+
| Arrow keys (in listbox) | Navigate mention suggestions |
|
|
115
|
+
| Enter / Space (in listbox) | Select mention suggestion |
|
|
116
|
+
| Escape (in listbox) | Dismiss mention list |
|
|
117
|
+
| F10 or Alt + F10 | Move focus from editable area to toolbar (accessibility shortcut) |
|
|
118
|
+
| Arrow keys (in toolbar) | Move between toolbar buttons (roving tabindex) |
|
|
119
|
+
| Home / End (in toolbar) | First / last toolbar button |
|
|
120
|
+
|
|
121
|
+
### Accessibility Rules
|
|
122
|
+
|
|
123
|
+
- Editable area MUST have `role="textbox"` — bare `contenteditable` is announced as a generic region by most AT
|
|
124
|
+
- `aria-multiline="true"` MUST be set — announces correct Enter-key behavior to screen readers
|
|
125
|
+
- Toolbar toggle buttons (bold, italic, lists) MUST use `aria-pressed="true/false"` to reflect current selection state
|
|
126
|
+
- Placeholder MUST use CSS `[data-placeholder]::before` content — not a `placeholder` HTML attribute on contenteditable (screen readers read it incorrectly or not at all)
|
|
127
|
+
- Keyboard shortcuts MUST be documented in a tooltip or help section — not assumed
|
|
128
|
+
- Read-only: set both `contenteditable="false"` AND `aria-readonly="true"` — contenteditable="false" alone is insufficient for AT
|
|
129
|
+
- Mention listbox MUST use `role="listbox"` with `role="option"` items; focused option uses `aria-selected="true"`
|
|
130
|
+
|
|
131
|
+
Cross-link: `reference/accessibility.md` — contenteditable accessibility, toolbar pattern.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Motion
|
|
136
|
+
|
|
137
|
+
| Transition | Duration | Easing | Notes |
|
|
138
|
+
|------------|----------|--------|-------|
|
|
139
|
+
| Toolbar button active state | 80ms | ease-out | Background fill on aria-pressed change |
|
|
140
|
+
| Mention list open | 150ms | ease-out | Fade + slide up from caret position |
|
|
141
|
+
| Mention list dismiss | 100ms | ease-in | Fade out |
|
|
142
|
+
| Bubble toolbar appear | 150ms | ease-out | Fade in above selection |
|
|
143
|
+
| Bubble toolbar dismiss | 80ms | ease-in | Fade out on deselect |
|
|
144
|
+
|
|
145
|
+
**BAN**: Do not animate text reflow on formatting changes — typing performance is critical; any CSS transition on editor content causes visual jitter.
|
|
146
|
+
|
|
147
|
+
Cross-link: `reference/motion.md` — reduced-motion: remove mention list slide; keep instant formatting toggle.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Do / Don't
|
|
152
|
+
|
|
153
|
+
### Do
|
|
154
|
+
- Add `role="textbox"` + `aria-multiline="true"` to the contenteditable element *(WAI-ARIA APG)*
|
|
155
|
+
- Use `aria-pressed` on all toggle toolbar buttons (bold, italic, list toggles) *(WAI-ARIA APG toolbar pattern)*
|
|
156
|
+
- Implement placeholder via CSS `::before` pseudo-element, not HTML attribute *(Tiptap, Atlassian)*
|
|
157
|
+
- Document keyboard shortcuts in a tooltip or accessible help dialog *(Atlassian Fabric Editor)*
|
|
158
|
+
|
|
159
|
+
### Don't
|
|
160
|
+
- Don't use bare `contenteditable` without `role="textbox"` — AT announces it as a generic landmark *(diverges from all 4 systems)*
|
|
161
|
+
- Don't omit `aria-pressed` on toggle buttons — AT users cannot determine current formatting state *(WAI-ARIA APG)*
|
|
162
|
+
- Don't use a `placeholder` HTML attribute on contenteditable — it is not a valid attribute and screen reader behavior is undefined *(MDN, Tiptap docs)*
|
|
163
|
+
- Don't suppress `outline` on the editable area without a custom focus-visible ring *(WCAG 2.4.7)*
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Anti-patterns Cross-links
|
|
168
|
+
|
|
169
|
+
| Anti-pattern | Entry |
|
|
170
|
+
|--------------|-------|
|
|
171
|
+
| BAN-06 | contenteditable without role="textbox" — `reference/anti-patterns.md#ban-06` |
|
|
172
|
+
| BAN-04 | transition: all on editor content causing reflow jank — `reference/anti-patterns.md#ban-04` |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Benchmark Citations
|
|
177
|
+
|
|
178
|
+
| Claim | Sources |
|
|
179
|
+
|-------|---------|
|
|
180
|
+
| contenteditable needs role="textbox" + aria-multiline="true" | WAI-ARIA spec, Tiptap a11y guide, Atlassian |
|
|
181
|
+
| Toolbar toggle buttons need aria-pressed | WAI-ARIA APG toolbar pattern |
|
|
182
|
+
| Placeholder via CSS ::before, not HTML attribute | Tiptap docs, Atlassian Fabric Editor |
|
|
183
|
+
| Mention list uses role="listbox" + role="option" | Tiptap mention extension, Lexical mention plugin |
|
|
184
|
+
| Ctrl/Cmd+B/I/U keyboard shortcuts are standard | Lexical, Tiptap, Slate — all implement these |
|
|
185
|
+
|
|
186
|
+
Full system URLs: `connections/design-corpora.md`
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Grep Signatures
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# contenteditable element missing role="textbox" (announced as generic region)
|
|
194
|
+
grep -rn 'contenteditable="true"' src/ | grep -v 'role="textbox"'
|
|
195
|
+
|
|
196
|
+
# Toolbar toggle buttons missing aria-pressed
|
|
197
|
+
grep -rn 'role="toolbar"' src/ -A 50 | grep 'role="button"' | grep -v 'aria-pressed'
|
|
198
|
+
|
|
199
|
+
# Placeholder set as HTML attribute on contenteditable (invalid)
|
|
200
|
+
grep -rn 'contenteditable' src/ | grep 'placeholder='
|
|
201
|
+
|
|
202
|
+
# Mention listbox missing role="listbox"
|
|
203
|
+
grep -rn 'mention\|@mention\|MentionList' src/ | grep -v 'role="listbox"'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Failing Example
|
|
209
|
+
|
|
210
|
+
```html
|
|
211
|
+
<!-- BAD: rich-text area using only contenteditable without role="textbox" -->
|
|
212
|
+
<div
|
|
213
|
+
contenteditable="true"
|
|
214
|
+
class="editor"
|
|
215
|
+
placeholder="Start writing..."
|
|
216
|
+
>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="toolbar">
|
|
219
|
+
<button onclick="document.execCommand('bold')">B</button>
|
|
220
|
+
<button onclick="document.execCommand('italic')">I</button>
|
|
221
|
+
</div>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Why it fails**: (1) Bare `contenteditable` is announced by most AT as a generic region, not a text input — users do not know they can type. (2) `placeholder` is not a valid HTML attribute on `<div>` — screen readers do not announce it. (3) Toolbar buttons have no `aria-pressed` — AT cannot determine if bold/italic is currently active. (4) `document.execCommand` is deprecated.
|
|
225
|
+
**Grep detection**: `grep -rn 'contenteditable="true"' src/ | grep -v 'role="textbox"'`
|
|
226
|
+
**Fix**: Add `role="textbox"` + `aria-multiline="true"` + `aria-label="Post body"` to the editable div; implement placeholder via CSS `[data-empty]::before { content: attr(data-placeholder) }`; add `aria-pressed="true/false"` to toolbar toggle buttons; use a modern editor library (Tiptap, Lexical) instead of execCommand.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Select / Combobox — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Radix UI, WAI-ARIA APG, Carbon, Headless UI, Mantine, Material 3, Ant Design, shadcn/ui
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A select allows the user to choose one (or multiple) options from a predefined list presented in a dropdown. A combobox extends this with a text filter input. Use native `<select>` when styling flexibility is not required and options are static; use a custom combobox when filtering, grouping, async loading, or complex option rendering is needed. *(Radix, Headless UI, WAI-ARIA APG agree on this decision tree)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Label *
|
|
18
|
+
┌────────────────────────┬──┐
|
|
19
|
+
│ Selected value │ ▾│ ← trigger button (role="combobox" + aria-haspopup)
|
|
20
|
+
└────────────────────────┴──┘
|
|
21
|
+
┌──────────────────────────┐
|
|
22
|
+
│ [search input] │ ← combobox only; role="textbox"
|
|
23
|
+
│──────────────────────────│
|
|
24
|
+
│ ○ Option A │ ← role="option" in role="listbox"
|
|
25
|
+
│ ○ Option B │
|
|
26
|
+
│ ○ Option C │
|
|
27
|
+
└──────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Part | Required | Notes |
|
|
31
|
+
|------|----------|-------|
|
|
32
|
+
| Label | Yes | `<label>` or `aria-label` on trigger |
|
|
33
|
+
| Trigger button | Yes | Announces selected value; opens dropdown on Enter/Space |
|
|
34
|
+
| Dropdown list | Yes | `role="listbox"` container |
|
|
35
|
+
| Option items | Yes | `role="option"` with `aria-selected` |
|
|
36
|
+
| Filter input | No | Combobox only; `role="combobox"` with `aria-controls` |
|
|
37
|
+
| Group headings | No | `role="group"` with `aria-label` |
|
|
38
|
+
| Empty state | Conditional | Required when async/filter can return zero results |
|
|
39
|
+
| Clear button | No | Clears selection; keyboard accessible |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Variants
|
|
44
|
+
|
|
45
|
+
| Variant | Description | Systems |
|
|
46
|
+
|---------|-------------|---------|
|
|
47
|
+
| Native select | `<select>` — minimal, accessible, no custom styling | All (as baseline) |
|
|
48
|
+
| Custom select | Styled trigger + listbox; no filter | Radix, Carbon, shadcn |
|
|
49
|
+
| Combobox | Trigger + text filter + listbox | Radix, Mantine, Headless UI, Ant |
|
|
50
|
+
| Multi-select | Multiple `aria-selected="true"` options; tag display | Mantine, Ant, Carbon |
|
|
51
|
+
| Async / searchable | Options loaded on query; loading state in listbox | Mantine, Ant, shadcn |
|
|
52
|
+
|
|
53
|
+
**Norm** (≥5/18): custom select must replicate native keyboard behavior exactly.
|
|
54
|
+
**Diverge**: tag vs. chip display for multi-select — Mantine uses tags, Ant uses chips, Carbon uses checkboxes in dropdown. Checkbox approach is most accessible (state is visible without removing focus from listbox).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## States
|
|
59
|
+
|
|
60
|
+
| State | Trigger | Visual | ARIA |
|
|
61
|
+
|-------|---------|--------|------|
|
|
62
|
+
| default | — | Resting trigger | `aria-expanded="false"` |
|
|
63
|
+
| open | trigger activated | Dropdown visible | `aria-expanded="true"` |
|
|
64
|
+
| option hovered | pointer | Option highlighted | `aria-activedescendant` updated |
|
|
65
|
+
| option selected | Enter / click | Checkmark or filled circle | `aria-selected="true"` |
|
|
66
|
+
| disabled | `disabled` attr | 38% opacity; pointer-events: none | `aria-disabled="true"` |
|
|
67
|
+
| error | validation | Red border + error message | `aria-invalid="true"` |
|
|
68
|
+
| loading | async fetch | Spinner inside listbox | `aria-busy="true"` on listbox |
|
|
69
|
+
| empty | filter = zero results | "No results" message in listbox | `aria-live="polite"` |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Sizing & Spacing
|
|
74
|
+
|
|
75
|
+
| Size | Trigger height | Option height | Padding H |
|
|
76
|
+
|------|---------------|---------------|-----------|
|
|
77
|
+
| sm | 32px | 28px | 12px |
|
|
78
|
+
| md (default) | 40px | 36px | 16px |
|
|
79
|
+
| lg | 48px | 44px | 16px |
|
|
80
|
+
|
|
81
|
+
Max dropdown height: 240px with internal scroll — prevents viewport overflow without pagination.
|
|
82
|
+
Option min-height: 36px (md) for touch targets *(Material 3, Polaris)*
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Typography
|
|
87
|
+
|
|
88
|
+
- Trigger value: same weight/size as input (14px/400)
|
|
89
|
+
- Option text: 14px/400; selected option: 14px/500
|
|
90
|
+
- Group label: 11px/600 uppercase, muted colour — not an option, not focusable
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Keyboard & Accessibility
|
|
95
|
+
|
|
96
|
+
> **WAI-ARIA role**: `combobox` on trigger (select pattern); `listbox` on dropdown; `option` on items
|
|
97
|
+
> **Required attributes**: `aria-expanded`, `aria-haspopup="listbox"`, `aria-controls` (trigger→listbox), `aria-activedescendant` (updated on option focus)
|
|
98
|
+
|
|
99
|
+
### Keyboard Contract — Select (Listbox Pattern)
|
|
100
|
+
|
|
101
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ — W3C — 2024*
|
|
102
|
+
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|-----|--------|
|
|
105
|
+
| Enter / Space | Opens listbox when trigger is focused |
|
|
106
|
+
| Escape | Closes listbox; returns focus to trigger |
|
|
107
|
+
| Arrow Down | Opens listbox (if closed) or moves focus to next option |
|
|
108
|
+
| Arrow Up | Opens listbox (if closed) or moves focus to previous option |
|
|
109
|
+
| Home | Moves focus to first option |
|
|
110
|
+
| End | Moves focus to last option |
|
|
111
|
+
| Enter (option focused) | Selects option; closes listbox |
|
|
112
|
+
| Tab | Closes listbox; moves focus to next element |
|
|
113
|
+
| Printable character | (Type-ahead) Moves focus to next option starting with typed character |
|
|
114
|
+
|
|
115
|
+
### Keyboard Contract — Combobox
|
|
116
|
+
|
|
117
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ — W3C — 2024*
|
|
118
|
+
|
|
119
|
+
| Key | Action |
|
|
120
|
+
|-----|--------|
|
|
121
|
+
| Any printable character | Filters option list; opens popup |
|
|
122
|
+
| Escape | Clears filter (if any); closes popup |
|
|
123
|
+
| Arrow Down | Moves focus into listbox (first or previously focused option) |
|
|
124
|
+
| Arrow Up | Moves focus to last option |
|
|
125
|
+
| Enter | Selects focused option; closes popup |
|
|
126
|
+
| Alt + Arrow Down | Opens popup without moving focus |
|
|
127
|
+
| Alt + Arrow Up | Closes popup; returns focus to textbox |
|
|
128
|
+
|
|
129
|
+
### Accessibility Rules
|
|
130
|
+
|
|
131
|
+
- `aria-activedescendant` on trigger MUST update as options are highlighted — screen readers follow this, not DOM focus
|
|
132
|
+
- Options must have unique `id` attributes (for `aria-activedescendant` reference)
|
|
133
|
+
- Grouping: `role="group"` with `aria-label` wrapping grouped `role="option"` items
|
|
134
|
+
- Empty state: announce via `aria-live="polite"` region when filter returns no results
|
|
135
|
+
- Virtualised lists: keep rendered DOM options in sync with `aria-activedescendant` pointer
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Motion
|
|
140
|
+
|
|
141
|
+
| Transition | Duration | Easing | Notes |
|
|
142
|
+
|------------|----------|--------|-------|
|
|
143
|
+
| Dropdown open | 120ms | ease-out | Scale 0.95→1 + fade; origin at trigger |
|
|
144
|
+
| Dropdown close | 80ms | ease-in | Fade only |
|
|
145
|
+
| Option highlight | 60ms | ease | Background colour only |
|
|
146
|
+
|
|
147
|
+
Cross-link: `reference/motion.md` — `AnimatePresence` pattern for mount/unmount
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Do / Don't
|
|
152
|
+
|
|
153
|
+
### Do
|
|
154
|
+
- Replicate native `<select>` keyboard behavior exactly in custom implementations *(WAI-ARIA APG, Radix)*
|
|
155
|
+
- Show a "No results" state (not empty dropdown) when filter has no matches *(Mantine, shadcn, Carbon)*
|
|
156
|
+
- Update `aria-activedescendant` on every option focus change *(WAI-ARIA APG)*
|
|
157
|
+
- Limit dropdown height to ~6–8 options; add scroll for more *(Carbon, Material 3)*
|
|
158
|
+
|
|
159
|
+
### Don't
|
|
160
|
+
- Don't close the dropdown on every keystroke in combobox mode — only on selection or Escape *(WAI-ARIA APG)*
|
|
161
|
+
- Don't use a select for navigation — use a nav + router *(Carbon, Primer)*
|
|
162
|
+
- Don't disable scroll inside the listbox — virtualise instead for large lists *(Mantine, Ant)*
|
|
163
|
+
- Don't place interactive elements (links, buttons) inside `role="option"` *(WAI-ARIA APG)*
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Anti-patterns Cross-links
|
|
168
|
+
|
|
169
|
+
| Anti-pattern | Entry |
|
|
170
|
+
|--------------|-------|
|
|
171
|
+
| Custom select without ARIA | `reference/anti-patterns.md` — ARIA role omission pattern |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Benchmark Citations
|
|
176
|
+
|
|
177
|
+
| Claim | Sources |
|
|
178
|
+
|-------|---------|
|
|
179
|
+
| aria-activedescendant pattern | WAI-ARIA APG combobox §4.1 |
|
|
180
|
+
| Escape closes + returns focus | WAI-ARIA APG listbox §3.4 |
|
|
181
|
+
| 240px max dropdown height | Carbon, Material 3 |
|
|
182
|
+
| Checkbox approach for multi-select a11y | Carbon, WAI-ARIA APG |
|
|
183
|
+
|
|
184
|
+
Full system URLs: `connections/design-corpora.md`
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Grep Signatures
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Custom select/combobox missing aria-expanded
|
|
192
|
+
grep -rn 'role="combobox"\|role="listbox"' src/ | grep -v 'aria-expanded'
|
|
193
|
+
|
|
194
|
+
# Options missing aria-selected
|
|
195
|
+
grep -rn 'role="option"' src/ | grep -v 'aria-selected'
|
|
196
|
+
|
|
197
|
+
# Missing aria-activedescendant on trigger
|
|
198
|
+
grep -rn 'role="combobox"' src/ | grep -v 'aria-activedescendant'
|
|
199
|
+
|
|
200
|
+
# Dropdown closed on every keypress (bad UX in combobox)
|
|
201
|
+
grep -rn 'onKeyDown\|on:keydown' src/ | grep -i 'select\|combobox\|dropdown'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Failing Example
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<!-- BAD: custom dropdown with no ARIA — keyboard users and screen readers are stranded -->
|
|
210
|
+
<div class="select-trigger" onclick="toggleDropdown()">Choose option</div>
|
|
211
|
+
<ul class="dropdown">
|
|
212
|
+
<li onclick="selectOption('a')">Option A</li>
|
|
213
|
+
<li onclick="selectOption('b')">Option B</li>
|
|
214
|
+
</ul>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Why it fails**: No role, no aria-expanded, no keyboard navigation, no focus management.
|
|
218
|
+
**Grep detection**: `grep -rn 'class.*dropdown\|class.*select' src/ | grep -v 'role='`
|
|
219
|
+
**Fix**: Use Radix `<Select>` or implement WAI-ARIA listbox pattern with `role="combobox"`, `role="listbox"`, `role="option"`, `aria-expanded`, and `aria-activedescendant`.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Sidebar (Collapsible Side Navigation Panel) — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon, Polaris, Atlassian, UUPM (app-interface, MIT)
|
|
4
|
+
**Wave**: 4 · **Category**: Navigation
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A sidebar provides persistent or collapsible secondary navigation along the vertical axis of an application. In expanded state it shows icon + label; in collapsed state it shows icon only (with a tooltip). It is distinct from a Drawer (which is a modal overlay — see `drawer.md`) and from a Navbar (primary horizontal navigation). Use a sidebar for application-level section switching and hierarchical settings navigation. *(Material 3 Navigation Drawer, Carbon UI Shell Left Nav, Atlassian SideNavigation, Polaris Navigation agree)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────┐ ┌────┐
|
|
18
|
+
│ [≡] App Name │ ◄──► │[≡] │ collapsed (icon-only)
|
|
19
|
+
│─────────────────│ │────│
|
|
20
|
+
│ 🏠 Dashboard │ │[🏠]│ tooltip: "Dashboard"
|
|
21
|
+
│ 📊 Analytics │ │[📊]│
|
|
22
|
+
│ ▾ Settings │ │[⚙] │
|
|
23
|
+
│ Account │ └────┘
|
|
24
|
+
│ Privacy │
|
|
25
|
+
│ Security │
|
|
26
|
+
└─────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Part | Required | Notes |
|
|
30
|
+
|------|----------|-------|
|
|
31
|
+
| `<nav>` wrapper | Yes | `role="navigation"` + `aria-label="Secondary"` |
|
|
32
|
+
| Toggle button | Yes | `aria-expanded` + `aria-controls` pointing to nav |
|
|
33
|
+
| Nav item | Yes | `<a>` for routing; `<button>` for non-routing actions |
|
|
34
|
+
| Sub-section toggle | No | `<button>` with `aria-expanded`; chevron icon rotates |
|
|
35
|
+
| Sub-section items | No | Indented child items; hidden when parent collapsed |
|
|
36
|
+
| Tooltip | Conditional | On icon-only items in collapsed state; `role="tooltip"` |
|
|
37
|
+
| Active indicator | Yes | `aria-current="page"` on active item |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Variants
|
|
42
|
+
|
|
43
|
+
| Variant | Description | Systems |
|
|
44
|
+
|---------|-------------|---------|
|
|
45
|
+
| Expanded | Icon + label visible; full width (240–280px) | All systems |
|
|
46
|
+
| Collapsed / mini | Icon only; 48–64px wide; tooltips on hover/focus | Material 3, Carbon, Atlassian |
|
|
47
|
+
| Floating / overlay | Overlay on top of content (mobile drawer pattern) | Material 3, Polaris |
|
|
48
|
+
| Settings nav | Category tree: Settings › Account › Privacy › Security | UUPM app-interface (MIT) |
|
|
49
|
+
| Dashboard nav | Section switcher: Dashboard / Analytics / Users / Settings | UUPM app-interface (MIT) |
|
|
50
|
+
|
|
51
|
+
**Norm** (≥4 systems agree): expanded width 240–280px; collapsed width 48–64px; toggle button at top or bottom.
|
|
52
|
+
**Diverge**: Carbon collapses to a rail (16px) with hover-expand; Material 3 differentiates between NavigationDrawer (permanent) and ModalNavigationDrawer (mobile).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## States
|
|
57
|
+
|
|
58
|
+
| State | Trigger | Visual | ARIA |
|
|
59
|
+
|-------|---------|--------|------|
|
|
60
|
+
| expanded | default or toggle | Full width, labels visible | `aria-expanded="true"` on toggle |
|
|
61
|
+
| collapsed | toggle click | Icon-only, labels hidden | `aria-expanded="false"` on toggle |
|
|
62
|
+
| item-default | — | Resting fill | — |
|
|
63
|
+
| item-hover | pointer over | 8% overlay | — |
|
|
64
|
+
| item-focus | keyboard focus | 2px focus-visible ring | — |
|
|
65
|
+
| item-active | current route | Filled pill or left indicator bar | `aria-current="page"` |
|
|
66
|
+
| subsection-open | button click | Children visible; chevron rotated 180° | `aria-expanded="true"` on section button |
|
|
67
|
+
| subsection-closed | button click | Children hidden | `aria-expanded="false"` on section button |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Sizing & Spacing
|
|
72
|
+
|
|
73
|
+
| State | Width | Item height | Item padding H |
|
|
74
|
+
|-------|-------|-------------|----------------|
|
|
75
|
+
| Expanded | 240–280px | 40px | 16px |
|
|
76
|
+
| Collapsed | 48–64px | 40px | 12px (centered icon) |
|
|
77
|
+
| Sub-item indent | — | 36px | 32px (16px base + 16px indent) |
|
|
78
|
+
|
|
79
|
+
**Norm**: 240px expanded width (Carbon, Atlassian, Polaris). 40px item height matches button defaults.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Typography
|
|
84
|
+
|
|
85
|
+
- Nav item label: body-sm (13–14px), weight 400; active item weight 500 or 600
|
|
86
|
+
- Sub-section heading (if present): label-xs (11–12px), uppercase, weight 600, `color: --text-subtle`
|
|
87
|
+
- Tooltip: body-xs (11–12px) — concise, matches item label exactly in collapsed state
|
|
88
|
+
- Never truncate nav item labels — resize the sidebar or abbreviate the label intentionally
|
|
89
|
+
|
|
90
|
+
Cross-link: `reference/typography.md` — label scale, body-sm
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Keyboard & Accessibility
|
|
95
|
+
|
|
96
|
+
> **WAI-ARIA role**: `navigation` (wrapper)
|
|
97
|
+
> **Required attributes**: `aria-label="Secondary"` on `<nav>`; `aria-expanded` on toggle button and collapsible section buttons; `aria-current="page"` on active item
|
|
98
|
+
|
|
99
|
+
### Keyboard Contract
|
|
100
|
+
|
|
101
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/ — W3C — 2024*
|
|
102
|
+
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|-----|--------|
|
|
105
|
+
| Tab / Shift+Tab | Moves focus through focusable items in DOM order |
|
|
106
|
+
| Enter / Space | Activates focused link (navigates) or button (toggles section/sidebar) |
|
|
107
|
+
| ArrowDown | Moves focus to next visible nav item (optional enhancement) |
|
|
108
|
+
| ArrowUp | Moves focus to previous visible nav item (optional enhancement) |
|
|
109
|
+
| Escape | Closes mobile overlay sidebar; returns focus to toggle |
|
|
110
|
+
|
|
111
|
+
### Accessibility Rules
|
|
112
|
+
|
|
113
|
+
- `<nav>` MUST have `aria-label="Secondary"` to distinguish from the primary navbar landmark
|
|
114
|
+
- Every collapsible sub-section MUST have `aria-expanded` on its toggle button
|
|
115
|
+
- Items that navigate (routing) MUST be `<a href>` links; items that only toggle state MUST be `<button>` (not `<a href="#">`)
|
|
116
|
+
- In collapsed state, each icon-only item MUST have a tooltip AND `aria-label` (tooltip is not a substitute for accessible name)
|
|
117
|
+
- `aria-current="page"` MUST be present on the currently active item
|
|
118
|
+
|
|
119
|
+
Cross-link: `reference/accessibility.md` — landmark labelling, disclosure pattern
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Motion
|
|
124
|
+
|
|
125
|
+
| Transition | Duration | Easing | Notes |
|
|
126
|
+
|------------|----------|--------|-------|
|
|
127
|
+
| Expand / collapse sidebar | 200ms | ease-in-out | Width transition; respect `prefers-reduced-motion` |
|
|
128
|
+
| Sub-section open | 150ms | ease-out | Height expand; `overflow: hidden` clip |
|
|
129
|
+
| Sub-section close | 120ms | ease-in | Height collapse |
|
|
130
|
+
| Chevron rotate | 150ms | ease-in-out | 0° → 180° on open |
|
|
131
|
+
| Label fade in | 100ms | ease-out | Delay until width ≥ 140px to prevent overlap |
|
|
132
|
+
|
|
133
|
+
**BAN**: Animating sidebar width using `transition: all` — catches unrelated property changes and causes jank.
|
|
134
|
+
|
|
135
|
+
Cross-link: `reference/motion.md` — layout transitions, BAN-04
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Do / Don't
|
|
140
|
+
|
|
141
|
+
### Do
|
|
142
|
+
- Use `<a href>` for routing nav items and `<button>` for non-routing actions *(Carbon, WAI-ARIA APG)*
|
|
143
|
+
- Label the `<nav>` with `aria-label="Secondary"` *(WAI-ARIA APG landmark regions)*
|
|
144
|
+
- Show tooltips on icon-only items in collapsed state *(Material 3, Carbon, Atlassian)*
|
|
145
|
+
- Use `aria-expanded` on sub-section toggles *(WAI-ARIA APG disclosure pattern)*
|
|
146
|
+
|
|
147
|
+
### Don't
|
|
148
|
+
- Don't use `<a href="#">` for items that don't navigate — breaks bookmark/open-in-new-tab expectations *(Carbon, WAI-ARIA)*
|
|
149
|
+
- Don't hide sub-items with `display: none` without also removing them from tab order *(WCAG 2.1.1)*
|
|
150
|
+
- Don't use the same `aria-label` on both sidebar nav and top navbar *(WAI-ARIA landmark uniqueness)*
|
|
151
|
+
- Don't collapse the sidebar below 44px touch target width on mobile *(WCAG 2.5.5)*
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Anti-patterns Cross-links
|
|
156
|
+
|
|
157
|
+
| Anti-pattern | Entry |
|
|
158
|
+
|--------------|-------|
|
|
159
|
+
| BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Benchmark Citations
|
|
164
|
+
|
|
165
|
+
| Claim | Sources |
|
|
166
|
+
|-------|---------|
|
|
167
|
+
| aria-label="Secondary" distinct from Primary | WAI-ARIA APG landmark regions, WCAG 4.1.2 |
|
|
168
|
+
| `<button>` for non-routing, `<a>` for routing | Carbon, WAI-ARIA APG, Primer |
|
|
169
|
+
| aria-expanded on collapsible sections | WAI-ARIA APG disclosure pattern |
|
|
170
|
+
| 240px expanded / 48–64px collapsed widths | Carbon, Atlassian, Polaris |
|
|
171
|
+
| Tooltip on icon-only collapsed items | Material 3, Carbon, Atlassian |
|
|
172
|
+
|
|
173
|
+
Full system URLs: `connections/design-corpora.md`
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Grep Signatures
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# nav element missing aria-label
|
|
181
|
+
grep -rn '<nav' src/ | grep -v 'aria-label\|aria-labelledby'
|
|
182
|
+
|
|
183
|
+
# Collapsible section missing aria-expanded
|
|
184
|
+
grep -rn 'sidebar\|sidenav\|side-nav' src/ | grep 'collapsible\|expandable\|toggle' | grep -v 'aria-expanded'
|
|
185
|
+
|
|
186
|
+
# href="#" on nav items (non-routing anchor misuse)
|
|
187
|
+
grep -rn 'href="#"' src/ | grep -i 'nav\|sidebar\|menu'
|
|
188
|
+
|
|
189
|
+
# Active item missing aria-current
|
|
190
|
+
grep -rn 'class.*active\|isActive\|is-active' src/ | grep -i 'nav.*item\|sidebar.*item' | grep -v 'aria-current'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Failing Example
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<!-- BAD: sidebar items using <a href="#"> for non-routing actions -->
|
|
199
|
+
<nav> <!-- missing aria-label -->
|
|
200
|
+
<a href="#">Dashboard</a>
|
|
201
|
+
<a href="#">Analytics</a>
|
|
202
|
+
<a href="#" class="active">Settings</a> <!-- no aria-current -->
|
|
203
|
+
<a href="#">
|
|
204
|
+
Account <!-- subsection, but no aria-expanded -->
|
|
205
|
+
</a>
|
|
206
|
+
</nav>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Why it fails**: `<nav>` has no label (ambiguous landmark); `<a href="#">` creates false navigation expectations and adds to browser history; active state uses CSS class only; no `aria-current="page"`; no `aria-expanded` for the sub-section.
|
|
210
|
+
**Grep detection**: `grep -rn 'href="#"' src/ | grep -i 'nav\|sidebar'`
|
|
211
|
+
**Fix**: Replace `<a href="#">` with `<button>` for non-routing items; add `aria-label="Secondary"` to `<nav>`; add `aria-current="page"` to active item; add `aria-expanded` to sub-section toggles.
|