@hegemonart/get-design-done 1.16.0 → 1.19.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 +12 -4
- package/.claude-plugin/plugin.json +22 -4
- package/CHANGELOG.md +111 -0
- package/README.md +27 -2
- package/agents/design-auditor.md +65 -1
- package/agents/design-context-builder.md +6 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +62 -0
- package/agents/design-phase-researcher.md +1 -1
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/package.json +16 -2
- package/reference/components/README.md +27 -23
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -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/file-upload.md +219 -0
- package/reference/components/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/rich-text-editor.md +226 -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/table.md +229 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/data-visualization.md +333 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/form-patterns.md +245 -0
- package/reference/image-optimization.md +582 -0
- package/reference/information-architecture.md +255 -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/onboarding-progressive-disclosure.md +250 -0
- package/reference/output-contracts/motion-map.schema.json +135 -0
- package/reference/platforms.md +346 -0
- package/reference/registry.json +445 -220
- package/reference/registry.schema.json +4 -0
- package/reference/rtl-cjk-cultural.md +353 -0
- package/reference/user-research.md +360 -0
- 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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Date Picker — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon Design System, Atlassian Design System, Mantine DatePicker
|
|
4
|
+
**Wave**: 5 · **Category**: Advanced
|
|
5
|
+
**Spec file**: `reference/components/date-picker.md`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
A Date Picker lets users select a single date or a date range through a calendar popover attached to a text input. It combines a formatted text field (for direct keyboard entry) with a visual month grid (for pointer or keyboard navigation). Use Date Picker when the date is human-meaningful and users may want to browse relative to a calendar context. Use a plain `<input type="date">` when native behavior is sufficient or a mobile-first audience is expected. *(Material 3, Carbon, Atlassian, Mantine agree: calendar popover + text input pairing is the canonical desktop pattern.)*
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Anatomy
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
[ Input field "MM/DD/YYYY" ] [ Calendar icon button ]
|
|
19
|
+
│
|
|
20
|
+
└── Popover (role="dialog", aria-modal="true")
|
|
21
|
+
├── Header: [ ← ] [ Month Year ] [ → ]
|
|
22
|
+
├── Day-of-week row (aria-hidden="true")
|
|
23
|
+
└── Month grid (role="grid")
|
|
24
|
+
└── Week rows (role="row")
|
|
25
|
+
└── Day cells (role="gridcell")
|
|
26
|
+
└── Day button (role="button")
|
|
27
|
+
|
|
28
|
+
Range variant adds a second input field for end date.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Part | Required | Notes |
|
|
32
|
+
|------|----------|-------|
|
|
33
|
+
| Text input | Yes | Shows selected date; accepts direct keyboard entry |
|
|
34
|
+
| Format hint label | Yes | Visible "MM/DD/YYYY" supplement — NOT placeholder only |
|
|
35
|
+
| Calendar trigger button | Yes | Opens/closes popover; `aria-label="Open calendar"` |
|
|
36
|
+
| Popover (dialog) | Yes | `role="dialog"` + `aria-modal="true"` + focus trap |
|
|
37
|
+
| Month navigation | Yes | Previous/next month buttons; keyboard Page Up/Down |
|
|
38
|
+
| Month grid | Yes | `role="grid"` |
|
|
39
|
+
| Day cell | Yes | `role="gridcell"` containing `role="button"` for selectable days |
|
|
40
|
+
| Range highlight | No (range variant only) | Visual fill between start and end dates |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Variants
|
|
45
|
+
|
|
46
|
+
| Variant | Description | Systems |
|
|
47
|
+
|---------|-------------|---------|
|
|
48
|
+
| Single date | One input + calendar popover | Material 3, Carbon, Atlassian, Mantine |
|
|
49
|
+
| Date range | Start + end input pair sharing one calendar | Carbon (DateRangePicker), Mantine (DateRangePicker), Atlassian |
|
|
50
|
+
| Date-time | Date selection + time selector panel | Material 3 (DateTimePicker), Mantine |
|
|
51
|
+
| Inline calendar | Calendar always visible, no popover | Mantine (Calendar), Material 3 (docked) |
|
|
52
|
+
|
|
53
|
+
**Norm** (≥3/4 systems agree): popover calendar attached to a text input is the dominant pattern; range uses two inputs sharing one calendar panel.
|
|
54
|
+
**Diverge**: Material 3 uses a modal dialog for mobile; Carbon and Mantine use an anchored popover for all viewports.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## States
|
|
59
|
+
|
|
60
|
+
| State | Trigger | Visual | ARIA |
|
|
61
|
+
|-------|---------|--------|------|
|
|
62
|
+
| default | — | Resting input + trigger icon | — |
|
|
63
|
+
| open | Trigger click or input focus + Enter | Popover visible, focus moves inside | `aria-expanded="true"` on trigger |
|
|
64
|
+
| day hover | Pointer over day | Background highlight | — |
|
|
65
|
+
| day focus | Keyboard navigation | Focus-visible ring on day button | — |
|
|
66
|
+
| day selected | Enter/Space or click | Filled background (brand color) | `aria-pressed="true"` on day button |
|
|
67
|
+
| range-in-progress | Start selected, end not yet | Partial fill from start | — |
|
|
68
|
+
| range-complete | Both start and end selected | Full highlight between dates | — |
|
|
69
|
+
| disabled date | Date excluded by min/max/filter | Muted; `tabindex="-1"` | `aria-disabled="true"` on day button |
|
|
70
|
+
| error | Invalid date typed | Red border + error message | `aria-invalid="true"` on input |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Sizing & Spacing
|
|
75
|
+
|
|
76
|
+
| Size | Input Height | Popover Width | Day Cell Size | Font |
|
|
77
|
+
|------|-------------|---------------|---------------|------|
|
|
78
|
+
| sm | 32px | 256px | 32px | 13px |
|
|
79
|
+
| md (default) | 40px | 288px | 40px | 14px |
|
|
80
|
+
| lg | 48px | 320px | 44px | 16px |
|
|
81
|
+
|
|
82
|
+
**Norm**: 288px popover width fits a standard 7-column month grid with comfortable padding *(Carbon, Mantine)*.
|
|
83
|
+
Day cells must be ≥32px to meet minimum touch-target guidance; prefer 40px for mixed pointer/touch contexts.
|
|
84
|
+
|
|
85
|
+
Cross-link: `reference/surfaces.md` — minimum 44×44px accessible tap target via padding.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Typography
|
|
90
|
+
|
|
91
|
+
- Input text: body-md weight 400; monospace or tabular-nums variant for date digits aids alignment
|
|
92
|
+
- Day numbers: body-sm, center-aligned within cell
|
|
93
|
+
- Month/year header: body-md weight 600
|
|
94
|
+
- Format hint ("MM/DD/YYYY"): caption-sm, secondary color — attached as visible `<label>` supplement or `<span aria-hidden="true">` paired with `aria-describedby` on the input
|
|
95
|
+
|
|
96
|
+
Cross-link: `reference/typography.md` — tabular-nums for date/time fields.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Keyboard & Accessibility
|
|
101
|
+
|
|
102
|
+
> **WAI-ARIA role**: `dialog` (popover), `grid` (month table), `button` (day cells)
|
|
103
|
+
> **Required attributes**: `role="dialog"` + `aria-modal="true"` + `aria-label="Choose date"` on popover; `role="grid"` on month table; `role="gridcell"` + `role="button"` on each day
|
|
104
|
+
|
|
105
|
+
### Keyboard Contract
|
|
106
|
+
|
|
107
|
+
*Adapted from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ and grid pattern — W3C — 2024*
|
|
108
|
+
|
|
109
|
+
| Key | Action |
|
|
110
|
+
|-----|--------|
|
|
111
|
+
| Enter / Space | Open calendar when focus is on trigger; select focused day |
|
|
112
|
+
| Arrow Left / Right | Move focus to previous / next day |
|
|
113
|
+
| Arrow Up / Down | Move focus to same day in previous / next week |
|
|
114
|
+
| Page Up | Navigate to previous month |
|
|
115
|
+
| Page Down | Navigate to next month |
|
|
116
|
+
| Ctrl + Page Up | Navigate to previous year |
|
|
117
|
+
| Ctrl + Page Down | Navigate to next year |
|
|
118
|
+
| Home | Move focus to first day of current week |
|
|
119
|
+
| End | Move focus to last day of current week |
|
|
120
|
+
| Escape | Close calendar popover; return focus to trigger button |
|
|
121
|
+
| Tab | Move focus through interactive elements inside popover |
|
|
122
|
+
|
|
123
|
+
### Accessibility Rules
|
|
124
|
+
|
|
125
|
+
- Calendar popover MUST use `role="dialog"` + `aria-modal="true"` — do not use a plain `<div>` overlay
|
|
126
|
+
- Focus MUST be trapped inside the open calendar; Escape closes and returns focus to the trigger
|
|
127
|
+
- Each selectable day MUST be a `<button>` (or element with `role="button"`) so keyboard and AT users can activate it
|
|
128
|
+
- Date format hint MUST be visible text, not only a placeholder (placeholder disappears on input and is not consistently announced)
|
|
129
|
+
- Disabled dates MUST use `aria-disabled="true"` and `tabindex="-1"` so they are skipped but still communicated to AT
|
|
130
|
+
- Range variant: announce selected range via `aria-label` on day buttons (e.g., `aria-label="April 15, 2025, start of range"`)
|
|
131
|
+
- Mobile: provide native `<input type="date">` fallback when touch device detected or user preference set
|
|
132
|
+
|
|
133
|
+
Cross-link: `reference/accessibility.md` — focus-trap pattern, dialog role requirements.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Motion
|
|
138
|
+
|
|
139
|
+
| Transition | Duration | Easing | Notes |
|
|
140
|
+
|------------|----------|--------|-------|
|
|
141
|
+
| Popover open | 150ms | ease-out | Fade + subtle scale from trigger |
|
|
142
|
+
| Popover close | 100ms | ease-in | Fade out |
|
|
143
|
+
| Month change | 200ms | ease-in-out | Slide left/right or cross-fade |
|
|
144
|
+
| Day selection | 80ms | ease-out | Fill background color |
|
|
145
|
+
| Range fill | 150ms | ease-out | Animate fill between dates |
|
|
146
|
+
|
|
147
|
+
**BAN**: Do not animate the calendar grid with a full-page slide — disorienting for keyboard users navigating by month.
|
|
148
|
+
|
|
149
|
+
Cross-link: `reference/motion.md` — reduced-motion: omit slide, keep instant day selection.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Do / Don't
|
|
154
|
+
|
|
155
|
+
### Do
|
|
156
|
+
- Show the date format as visible text ("MM/DD/YYYY") near the input *(Carbon, Atlassian)*
|
|
157
|
+
- Ensure keyboard users can navigate the entire calendar without a pointer *(WAI-ARIA APG)*
|
|
158
|
+
- Allow direct text entry in the input field — some users know the date and do not need the calendar *(Mantine, Carbon)*
|
|
159
|
+
- Support locale-aware first day of week (Sunday vs. Monday) and locale month/day names *(Material 3, Mantine)*
|
|
160
|
+
|
|
161
|
+
### Don't
|
|
162
|
+
- Don't use `<table>` with click-only cells — add `role="grid"` and full keyboard navigation *(WCAG 2.1 §2.1.1)*
|
|
163
|
+
- Don't use placeholder text alone to convey the date format — placeholder vanishes on input *(Carbon, Atlassian)*
|
|
164
|
+
- Don't trap focus permanently — Escape must always close the popover and restore focus *(WAI-ARIA APG)*
|
|
165
|
+
- Don't omit the native `<input type="date">` for mobile — custom calendars are unusable on small touch screens *(Material 3)*
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Anti-patterns Cross-links
|
|
170
|
+
|
|
171
|
+
| Anti-pattern | Entry |
|
|
172
|
+
|--------------|-------|
|
|
173
|
+
| BAN-07 | Placeholder as only label/hint — `reference/anti-patterns.md#ban-07` |
|
|
174
|
+
| BAN-12 | Custom overlay without focus trap — `reference/anti-patterns.md#ban-12` |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Benchmark Citations
|
|
179
|
+
|
|
180
|
+
| Claim | Sources |
|
|
181
|
+
|-------|---------|
|
|
182
|
+
| Popover uses role="dialog" + aria-modal | WAI-ARIA APG dialog pattern, Carbon, Atlassian |
|
|
183
|
+
| Month grid uses role="grid" | WAI-ARIA APG grid pattern, Mantine docs |
|
|
184
|
+
| Arrow keys navigate days; Page Up/Down change month | WAI-ARIA APG, Carbon DatePicker keyboard docs |
|
|
185
|
+
| Format hint must be visible text, not only placeholder | Carbon, Atlassian design guidelines |
|
|
186
|
+
| Native input fallback for mobile | Material 3, Mantine (mobile prop) |
|
|
187
|
+
|
|
188
|
+
Full system URLs: `connections/design-corpora.md`
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Grep Signatures
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Calendar popover missing role="dialog" (uses plain div overlay)
|
|
196
|
+
grep -rn 'calendar\|datepicker\|date-picker' src/ | grep -v 'role="dialog"'
|
|
197
|
+
|
|
198
|
+
# Day buttons missing keyboard handler (click-only calendar)
|
|
199
|
+
grep -rn 'role="gridcell"\|\.day\|\.calendar-day' src/ | grep -v 'onKeyDown\|on:keydown\|handleKey'
|
|
200
|
+
|
|
201
|
+
# Date input using placeholder for format hint only (no visible label supplement)
|
|
202
|
+
grep -rn '<input.*type="text".*date\|DateInput' src/ | grep 'placeholder.*MM\|placeholder.*dd' | grep -v 'aria-describedby\|<label\|hint'
|
|
203
|
+
|
|
204
|
+
# Missing aria-modal on calendar popover
|
|
205
|
+
grep -rn 'role="dialog"' src/ | grep -v 'aria-modal'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Failing Example
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<!-- BAD: calendar using <table> with click-only day cells, no keyboard navigation -->
|
|
214
|
+
<table class="calendar">
|
|
215
|
+
<tbody>
|
|
216
|
+
<tr>
|
|
217
|
+
<td class="day" onclick="selectDate('2025-04-01')">1</td>
|
|
218
|
+
<td class="day" onclick="selectDate('2025-04-02')">2</td>
|
|
219
|
+
<!-- ... -->
|
|
220
|
+
</tr>
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Why it fails**: `<td>` cells are not focusable by default; no keyboard navigation; AT users cannot select dates; no `role="grid"` or `role="gridcell"`; click handler is the only interaction path.
|
|
226
|
+
**Grep detection**: `grep -rn '<table.*calendar\|<td.*onclick.*date\|<td.*selectDate' src/`
|
|
227
|
+
**Fix**: Replace with `<table role="grid">`, `<td role="gridcell">`, `<button>` inside each cell, and full Arrow/Page/Home/End keyboard handlers. Add `role="dialog"` + `aria-modal="true"` + focus trap on the popover wrapper.
|
|
@@ -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.
|