@hegemonart/get-design-done 1.15.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.claude-plugin/marketplace.json +9 -5
  2. package/.claude-plugin/plugin.json +19 -5
  3. package/CHANGELOG.md +122 -0
  4. package/README.md +41 -0
  5. package/SKILL.md +4 -1
  6. package/agents/component-benchmark-harvester.md +112 -0
  7. package/agents/component-benchmark-synthesizer.md +88 -0
  8. package/agents/design-auditor.md +60 -1
  9. package/agents/design-doc-writer.md +21 -0
  10. package/agents/design-executor.md +22 -4
  11. package/agents/design-pattern-mapper.md +61 -0
  12. package/agents/motion-mapper.md +74 -9
  13. package/agents/token-mapper.md +8 -0
  14. package/connections/design-corpora.md +158 -0
  15. package/package.json +13 -3
  16. package/reference/components/README.md +94 -0
  17. package/reference/components/TEMPLATE.md +184 -0
  18. package/reference/components/accordion.md +217 -0
  19. package/reference/components/alert.md +198 -0
  20. package/reference/components/badge.md +202 -0
  21. package/reference/components/breadcrumbs.md +198 -0
  22. package/reference/components/button.md +195 -0
  23. package/reference/components/card.md +200 -0
  24. package/reference/components/checkbox.md +207 -0
  25. package/reference/components/chip.md +209 -0
  26. package/reference/components/command-palette.md +228 -0
  27. package/reference/components/date-picker.md +227 -0
  28. package/reference/components/drawer.md +201 -0
  29. package/reference/components/file-upload.md +219 -0
  30. package/reference/components/input.md +208 -0
  31. package/reference/components/label.md +200 -0
  32. package/reference/components/link.md +193 -0
  33. package/reference/components/list.md +217 -0
  34. package/reference/components/menu.md +212 -0
  35. package/reference/components/modal-dialog.md +210 -0
  36. package/reference/components/navbar.md +211 -0
  37. package/reference/components/pagination.md +205 -0
  38. package/reference/components/popover.md +197 -0
  39. package/reference/components/progress.md +210 -0
  40. package/reference/components/radio.md +203 -0
  41. package/reference/components/rich-text-editor.md +226 -0
  42. package/reference/components/select-combobox.md +219 -0
  43. package/reference/components/sidebar.md +211 -0
  44. package/reference/components/skeleton.md +197 -0
  45. package/reference/components/slider.md +208 -0
  46. package/reference/components/stepper.md +220 -0
  47. package/reference/components/switch.md +194 -0
  48. package/reference/components/table.md +229 -0
  49. package/reference/components/tabs.md +213 -0
  50. package/reference/components/toast.md +200 -0
  51. package/reference/components/tooltip.md +201 -0
  52. package/reference/components/tree.md +225 -0
  53. package/reference/css-grid-layout.md +835 -0
  54. package/reference/external/NOTICE.hyperframes +28 -0
  55. package/reference/image-optimization.md +582 -0
  56. package/reference/motion-advanced.md +754 -0
  57. package/reference/motion-easings.md +381 -0
  58. package/reference/motion-interpolate.md +282 -0
  59. package/reference/motion-spring.md +234 -0
  60. package/reference/motion-transition-taxonomy.md +155 -0
  61. package/reference/motion.md +20 -0
  62. package/reference/output-contracts/motion-map.schema.json +135 -0
  63. package/reference/registry.json +285 -0
  64. package/reference/registry.schema.json +6 -1
  65. package/reference/variable-fonts-loading.md +532 -0
  66. package/scripts/lib/easings.cjs +280 -0
  67. package/scripts/lib/parse-contract.cjs +220 -0
  68. package/scripts/lib/spring.cjs +160 -0
  69. package/scripts/tests/test-motion-provenance.sh +64 -0
  70. package/skills/benchmark/SKILL.md +105 -0
@@ -0,0 +1,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,208 @@
1
+ # Input — Benchmark Spec
2
+
3
+ **Harvested from**: Material 3, Carbon, Ant Design, Mantine, Polaris, Fluent 2, Atlassian, shadcn/ui
4
+ **Wave**: 1 · **Category**: Inputs
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A single-line text input collects short textual data from the user. It always has an associated visible label (never placeholder-only), optionally shows a helper text or character count below, and surfaces error state with an accessible message. Multi-line content belongs in a textarea; structured data (dates, phones) may warrant a specialised input type.
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ Label * ← <label for="id"> — always visible
18
+ ┌─────────────────────────┐
19
+ │ placeholder / value │ ← <input type="text"> or type="search" / "email" / etc.
20
+ └─────────────────────────┘
21
+ Helper text / Error msg ← aria-describedby linked
22
+ Character count (opt.) ← aria-live="polite" region
23
+ ```
24
+
25
+ | Part | Required | Notes |
26
+ |------|----------|-------|
27
+ | Label | Yes | Visible; never placeholder-only |
28
+ | Input element | Yes | Native `<input>` preferred; `type` set explicitly |
29
+ | Helper text | No | Persistent instructional text below input |
30
+ | Error message | Conditional | Shown on invalid state; replaces or joins helper text |
31
+ | Character count | No | `aria-live="polite"` region; announced on pause |
32
+ | Required indicator | No | `*` with `aria-required="true"` on input; legend explains `*` |
33
+ | Leading icon / adornment | No | 16–20px; left-inset with 12px gap from text |
34
+ | Trailing icon / clear button | No | Clear action must be keyboard-accessible |
35
+
36
+ ---
37
+
38
+ ## Variants
39
+
40
+ | Variant | Description | Systems |
41
+ |---------|-------------|---------|
42
+ | Outlined | Border box with floating/static label | Material 3, Ant, Mantine, shadcn |
43
+ | Filled | Filled background, underline only | Material 3, Carbon |
44
+ | Underline / Simple | Bottom border only | Carbon (fluid), Fluent |
45
+ | Search | Leading search icon; clear button on value | All systems |
46
+ | Password | Trailing show/hide toggle | All systems |
47
+ | Number | `type="number"` or `inputmode="numeric"` | Material 3, Ant, Mantine |
48
+
49
+ **Norm** (≥6/18): outlined with floating or static label is the most-cited default.
50
+ **Diverge**: floating vs. static label — Material 3 uses floating; Carbon, Polaris, Atlassian use static (above). Static label is safer for a11y (floating requires JavaScript + ARIA management).
51
+
52
+ ---
53
+
54
+ ## States
55
+
56
+ | State | Trigger | Visual | ARIA |
57
+ |-------|---------|--------|------|
58
+ | default | — | Resting border | — |
59
+ | hover | pointer over | Border lightens 20% | — |
60
+ | focus | keyboard / click | 2px focus-visible ring or thickened border | — |
61
+ | filled | has value | Label lifts (floating) or stays static | — |
62
+ | disabled | `disabled` attr | 38% opacity; cursor: not-allowed | `disabled` attr |
63
+ | read-only | `readonly` attr | No border change; cursor: default | `readonly` attr |
64
+ | error | invalid | Red/error border + icon + error message | `aria-invalid="true"` + `aria-describedby` |
65
+ | success | valid (opt.) | Green border + check icon | — |
66
+
67
+ ---
68
+
69
+ ## Sizing & Spacing
70
+
71
+ | Size | Height | Padding H | Font | Label size |
72
+ |------|--------|-----------|------|------------|
73
+ | sm | 32px | 12px | 13px | 12px |
74
+ | md (default) | 40px | 16px | 14px | 14px |
75
+ | lg | 48px | 16px | 16px | 16px |
76
+
77
+ **Norm**: 40px default height (Carbon, Polaris, Fluent, Atlassian confirm).
78
+ Minimum width: 200px — narrower inputs invite input truncation and frustrate users.
79
+
80
+ Cross-link: `reference/surfaces.md` — hit area ≥44px via padding; `reference/typography.md` — label sizing.
81
+
82
+ ---
83
+
84
+ ## Typography
85
+
86
+ - Label: 14px/500 above input; 12px when floating in focus/filled state
87
+ - Placeholder: 14px/400; color at 40% contrast minimum — never the only label
88
+ - Helper/error: 12px/400; full contrast for error messages
89
+ - **Placeholder is not a label**: it disappears on type, fails contrast, and cannot be announced by screen readers as a persistent label
90
+
91
+ Cross-link: `reference/typography.md` — text-wrap, font-smoothing rules
92
+
93
+ ---
94
+
95
+ ## Keyboard & Accessibility
96
+
97
+ > **WAI-ARIA role**: `textbox` (implicit on `<input type="text">`)
98
+ > **Required attributes**: `id` + matching `<label for>`, or `aria-label`; `aria-describedby` linking error/helper
99
+
100
+ ### Keyboard Contract
101
+
102
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/textbox/ — W3C — 2024*
103
+
104
+ | Key | Action |
105
+ |-----|--------|
106
+ | Any printable character | Types character into field |
107
+ | Backspace / Delete | Removes character |
108
+ | Home | Moves caret to start |
109
+ | End | Moves caret to end |
110
+ | Ctrl+A | Selects all |
111
+ | Tab | Moves focus to next element |
112
+ | Shift+Tab | Moves focus to previous element |
113
+
114
+ Password toggle and clear button must be keyboard accessible (Enter/Space activate).
115
+
116
+ ### Accessibility Rules
117
+
118
+ - Label MUST be associated via `<label for="id">` or `aria-label` — `placeholder` alone is not sufficient
119
+ - Error message MUST be linked via `aria-describedby` and triggered before or alongside visual indicator
120
+ - `aria-invalid="true"` MUST be set on the input when in error state
121
+ - `aria-required="true"` for required fields (supplement with visual `*` + legend)
122
+ - Character count region: `aria-live="polite"` to avoid over-announcing on every keystroke
123
+
124
+ Cross-link: `reference/accessibility.md`
125
+
126
+ ---
127
+
128
+ ## Motion
129
+
130
+ | Transition | Duration | Easing | Notes |
131
+ |------------|----------|--------|-------|
132
+ | label float | 150ms | ease-out | Floating label only; avoid if complex JS needed |
133
+ | border colour | 100ms | ease | Focus/error state border change |
134
+ | error message in | 150ms | ease-out | Slide-down + fade; respect prefers-reduced-motion |
135
+
136
+ Cross-link: `reference/motion.md` — `prefers-reduced-motion` guard required on label float
137
+
138
+ ---
139
+
140
+ ## Do / Don't
141
+
142
+ ### Do
143
+ - Always show a visible label above or beside the input *(Carbon, Polaris, Atlassian, WAI-ARIA APG)*
144
+ - Show inline error messages immediately below the failing field *(Material 3, Carbon, Polaris)*
145
+ - Associate helper text and errors via `aria-describedby` *(WAI-ARIA APG)*
146
+ - Use `autocomplete` attributes for common fields (name, email, address) *(Polaris, Fluent)*
147
+
148
+ ### Don't
149
+ - Don't use `placeholder` as the only label — it disappears and fails contrast *(Carbon, Polaris, Atlassian)*
150
+ - Don't show error state before the user has had a chance to input (premature validation) *(Polaris)*
151
+ - Don't remove the label on focus to create space — floating labels break screen readers *(Atlassian)*
152
+ - Don't use `type="number"` for things that aren't math operands (phone, ZIP) — use `inputmode` instead *(Mantine, Carbon)*
153
+
154
+ ---
155
+
156
+ ## Anti-patterns Cross-links
157
+
158
+ | Anti-pattern | Entry |
159
+ |--------------|-------|
160
+ | Placeholder-as-label | `reference/anti-patterns.md` — no dedicated BAN yet; cross-ref accessibility.md |
161
+
162
+ ---
163
+
164
+ ## Benchmark Citations
165
+
166
+ | Claim | Sources |
167
+ |-------|---------|
168
+ | 40px default height | Carbon, Polaris, Fluent 2, Atlassian |
169
+ | Placeholder not a label | Carbon, Polaris, Atlassian, WAI-ARIA APG |
170
+ | aria-describedby for errors | WAI-ARIA APG, Carbon, Mantine |
171
+ | Static label safer than floating | Atlassian, Carbon, Polaris |
172
+
173
+ Full system URLs: `connections/design-corpora.md`
174
+
175
+ ---
176
+
177
+ ## Grep Signatures
178
+
179
+ ```bash
180
+ # Placeholder-as-label (no <label> associated)
181
+ grep -rn 'placeholder=' src/ | grep -v 'aria-label\|<label'
182
+
183
+ # Missing aria-invalid on error state
184
+ grep -rn 'error\|invalid' src/ | grep '<input' | grep -v 'aria-invalid'
185
+
186
+ # Missing aria-describedby on input with helper/error
187
+ grep -rn '<input' src/ | grep -v 'aria-describedby'
188
+
189
+ # type="number" on non-numeric semantic fields
190
+ grep -rn 'type="number"' src/ | grep -i 'phone\|zip\|postal\|card'
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Failing Example
196
+
197
+ ```html
198
+ <!-- BAD: placeholder as label — disappears on type, fails contrast, not announced persistently -->
199
+ <input type="text" placeholder="Email address" />
200
+ ```
201
+
202
+ **Why it fails**: Placeholder has 40% opacity (below 4.5:1 AA), disappears when user types, and screen readers do not treat it as a persistent label.
203
+ **Grep detection**: `grep -rn '<input' src/ | grep 'placeholder=' | grep -v 'aria-label\|id='`
204
+ **Fix**:
205
+ ```html
206
+ <label for="email">Email address</label>
207
+ <input type="email" id="email" autocomplete="email" />
208
+ ```
@@ -0,0 +1,200 @@
1
+ # Label — Benchmark Spec
2
+
3
+ **Harvested from**: WAI-ARIA APG, Carbon, Material 3, Mantine, Polaris, Atlassian, Fluent 2, shadcn/ui
4
+ **Wave**: 1 · **Category**: Inputs
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A label is the visible text that identifies a form control to the user and to assistive technology. It is the most critical accessibility primitive in forms — every input, select, checkbox, radio, and switch MUST have an associated label. Labels are distinct from placeholders (which disappear) and from hints (which supplement but do not replace). *(WAI-ARIA APG, Carbon, Polaris, Atlassian all agree)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ Label text * ← <label for="id"> (static, above control)
18
+ ┌──────────────────┐
19
+ │ Input │ ← <input id="id">
20
+ └──────────────────┘
21
+ Helper text
22
+
23
+ Alternative (legend for group):
24
+ <fieldset>
25
+ <legend>Group label</legend> ← <legend> replaces <label> for groups
26
+ ...controls...
27
+ </fieldset>
28
+ ```
29
+
30
+ | Part | Required | Notes |
31
+ |------|----------|-------|
32
+ | Label text | Yes | Visible; descriptive; ≤40 chars preferred |
33
+ | Required indicator | Conditional | `*` or "(required)"; always explained near form |
34
+ | Optional indicator | Conditional | "(optional)" text is clearer than required asterisk |
35
+ | Helper text | No | Below control; `aria-describedby` |
36
+ | `for` / `id` association | Yes | OR `aria-label` / `aria-labelledby` on control |
37
+ | Legend (groups) | Yes (groups) | Replaces `<label>` for `<fieldset>` groups |
38
+
39
+ ---
40
+
41
+ ## Variants
42
+
43
+ | Variant | Description | Systems |
44
+ |---------|-------------|---------|
45
+ | Static label (above) | Fixed position above control — most accessible | Carbon, Polaris, Atlassian, Fluent |
46
+ | Floating label | Starts inside control, floats up on focus/fill | Material 3, Mantine, shadcn |
47
+ | Inline label | Label beside control (radio/checkbox) | All |
48
+ | Legend | Group label inside `<fieldset>` | WAI-ARIA APG, all (for groups) |
49
+ | Visually hidden | Accessible but not visible (e.g., search icon button) | WAI-ARIA APG, Carbon |
50
+
51
+ **Norm** (≥5/18): static label above the control is the most accessible and implementation-simple approach — recommended as the default.
52
+ **Diverge**: floating label — Material 3 and Mantine use it; Carbon, Polaris, Atlassian explicitly recommend static labels for a11y predictability. Floating labels require JavaScript, break if JS fails, and require careful `aria-*` management.
53
+
54
+ ---
55
+
56
+ ## States
57
+
58
+ Labels are not interactive — they have no hover/focus states of their own. However:
59
+
60
+ | Control State | Label Behaviour |
61
+ |---------------|-----------------|
62
+ | error | Label colour may shift to error colour (optional); error text replaces/appends helper |
63
+ | disabled | Label at 38% opacity alongside disabled control |
64
+ | required | Required indicator (`*`) added — never remove from DOM |
65
+ | focus (on control) | Label may shift colour to primary (Material 3 floating) |
66
+
67
+ ---
68
+
69
+ ## Sizing & Spacing
70
+
71
+ | Property | Value | Notes |
72
+ |----------|-------|-------|
73
+ | Font size | 14px (static); 12px (floating — small state) | |
74
+ | Weight | 500 | Slightly heavier than body to distinguish |
75
+ | Gap: label → control | 4–8px | *(Carbon: 4px, Material 3: 8px)* |
76
+ | Required asterisk gap | 2px left of asterisk | |
77
+ | Width | Match control width | Labels should not exceed their control |
78
+
79
+ Cross-link: `reference/typography.md` — label sizing rules
80
+
81
+ ---
82
+
83
+ ## Typography
84
+
85
+ - Label text: 14px/500 — slightly heavier than body 400; distinguishes from surrounding content
86
+ - Required `*`: same size, colour matches error or primary brand colour
87
+ - Visually-hidden labels: use CSS `.sr-only` pattern (clip + overflow: hidden + absolute), never `display:none` or `visibility:hidden`
88
+
89
+ ```css
90
+ /* sr-only — label hidden visually but present for screen readers */
91
+ .sr-only {
92
+ position: absolute;
93
+ width: 1px; height: 1px;
94
+ padding: 0; margin: -1px;
95
+ overflow: hidden;
96
+ clip: rect(0,0,0,0);
97
+ white-space: nowrap;
98
+ border: 0;
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Keyboard & Accessibility
105
+
106
+ > **WAI-ARIA role**: `label` (implicit on `<label>`)
107
+ > **Required attributes**: `for="control-id"` on `<label>`, matching `id` on control
108
+
109
+ ### Association Methods (in order of preference)
110
+
111
+ *Per WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/ — W3C — 2024*
112
+
113
+ 1. **`<label for="id">`** — native HTML; best browser + AT support; clicking label focuses control
114
+ 2. **`aria-labelledby="label-id"`** — when label cannot use `for` (complex composites)
115
+ 3. **`aria-label="string"`** — when no visible label is possible (icon-only controls); last resort
116
+ 4. **`<legend>` inside `<fieldset>`** — for groups of related controls; not replaceable by `aria-label`
117
+
118
+ ### Accessibility Rules
119
+
120
+ - NEVER use `placeholder` as the only label — it disappears on input and fails colour contrast *(WAI-ARIA APG, WCAG 1.3.1)*
121
+ - Required fields: mark with `aria-required="true"` on the control AND `*` visually; provide a form-level note explaining the `*` convention
122
+ - Optional fields: prefer marking optional fields with "(optional)" text over marking every required field with `*` — reduces asterisk clutter in long forms *(Polaris, Carbon)*
123
+ - Group labels: `<legend>` inside `<fieldset>` is the ONLY proper group label technique — `aria-label` on a `<div>` group is inadequate for radio/checkbox groups in most AT
124
+ - Visually hidden labels: use `.sr-only` CSS — never `display:none` (removes from AT tree) or `visibility:hidden`
125
+
126
+ ---
127
+
128
+ ## Do / Don't
129
+
130
+ ### Do
131
+ - Place labels above controls, not beside them, for forms wider than 240px *(Carbon, Polaris, Atlassian)*
132
+ - Use `<label for="id">` — the click zone extends to the full label, improving usability *(WAI-ARIA APG, all)*
133
+ - Explain the `*` required indicator once near the top of the form *(Polaris, Carbon)*
134
+ - Use `<legend>` for groups — it is read before each option in the group *(WAI-ARIA APG)*
135
+
136
+ ### Don't
137
+ - Don't use `placeholder` as the only label — it fails at 3 accessibility criteria *(WAI-ARIA APG, WCAG 1.3.1, 1.4.3)*
138
+ - Don't use `display:none` on labels — removes them from the AT accessibility tree *(WAI-ARIA APG)*
139
+ - Don't write labels as questions ("What is your name?") — prefer noun phrases ("Full name") *(Polaris, Carbon)*
140
+ - Don't truncate label text — ellipsis hides required information from all users *(Atlassian, Carbon)*
141
+
142
+ ---
143
+
144
+ ## Anti-patterns Cross-links
145
+
146
+ | Anti-pattern | Entry |
147
+ |--------------|-------|
148
+ | Placeholder-as-label | `reference/anti-patterns.md` |
149
+ | display:none on accessible label | `reference/anti-patterns.md` |
150
+
151
+ ---
152
+
153
+ ## Benchmark Citations
154
+
155
+ | Claim | Sources |
156
+ |-------|---------|
157
+ | `<label for>` clicking focuses control | HTML spec, WAI-ARIA APG |
158
+ | legend for group labels (not aria-label) | WAI-ARIA APG, Carbon |
159
+ | Static label above preferred over floating | Carbon, Polaris, Atlassian |
160
+ | .sr-only pattern for hidden labels | WAI-ARIA APG, Carbon, Tailwind |
161
+ | placeholder fails 3 a11y criteria | WAI-ARIA APG, WCAG 1.3.1, 1.4.3 |
162
+
163
+ Full system URLs: `connections/design-corpora.md`
164
+
165
+ ---
166
+
167
+ ## Grep Signatures
168
+
169
+ ```bash
170
+ # Input with no associated label (no for/id, no aria-label)
171
+ grep -rn '<input' src/ | grep -v 'type="hidden"\|type="submit"\|type="button"' \
172
+ | grep -v 'id=\|aria-label\|aria-labelledby'
173
+
174
+ # Label using display:none (removed from AT tree)
175
+ grep -rn 'display:\s*none\|display:none' src/ | grep -i 'label\|<label'
176
+
177
+ # Placeholder used without separate label
178
+ grep -rn 'placeholder=' src/ | grep -v 'aria-label\|<label\|aria-labelledby'
179
+
180
+ # Group without fieldset/legend
181
+ grep -rn 'type="radio"\|type="checkbox"' src/ | grep -v 'fieldset\|legend'
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Failing Example
187
+
188
+ ```html
189
+ <!-- BAD: label using display:none — completely removed from accessibility tree -->
190
+ <label for="search" style="display:none">Search</label>
191
+ <input type="text" id="search" placeholder="Search…">
192
+ ```
193
+
194
+ **Why it fails**: `display:none` removes the label from the DOM accessibility tree. Screen readers see only the placeholder (which disappears on type and has low contrast). The input has no persistent accessible name.
195
+ **Grep detection**: `grep -rn 'display:.*none' src/ | grep '<label\|label.*for'`
196
+ **Fix**:
197
+ ```html
198
+ <label for="search" class="sr-only">Search</label>
199
+ <input type="text" id="search" placeholder="Search products…">
200
+ ```