@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,202 @@
1
+ # Badge — Benchmark Spec
2
+
3
+ **Harvested from**: Material 3, Polaris, Carbon, Radix
4
+ **Wave**: 3 · **Category**: Feedback
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A badge is a compact numeric counter or status indicator overlaid on or beside a parent element (icon, avatar, button) to communicate a count (unread messages, notification count) or status (online, offline, busy). It is purely decorative — the accessible count or status is surfaced through the parent element's `aria-label`. *(Material 3, Polaris, Carbon, Radix agree: badge is decorative; parent carries accessible state)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ **Attached (overlay)**
17
+ ```
18
+ ┌──────────────────────┐
19
+ │ [icon/avatar] │
20
+ │ ┌──┐│
21
+ │ │ 3││ ← badge, position: absolute top-right
22
+ │ └──┘│
23
+ └──────────────────────┘
24
+ ↑ parent: aria-label="Messages, 3 unread"
25
+ ```
26
+
27
+ **Standalone**
28
+ ```
29
+ ┌──────┐
30
+ │ 99+ │ ← badge standalone (rare; decorative in a list)
31
+ └──────┘
32
+ ```
33
+
34
+ | Part | Required | Notes |
35
+ |------|----------|-------|
36
+ | Badge element | Yes | Pill or circle shape containing count or dot |
37
+ | Count / label text | Conditional | Present for count/icon variants; absent for dot |
38
+ | Parent element | Yes (for attached) | Carries `aria-label` with accessible count |
39
+ | Dot indicator | No | Status dot variant — no number, pure color/shape |
40
+
41
+ ---
42
+
43
+ ## Variants
44
+
45
+ | Variant | Description | Systems |
46
+ |---------|-------------|---------|
47
+ | Count | Numeric; shows integer up to 99, then "99+" | Material 3, Polaris, Carbon, Radix |
48
+ | Dot | Status indicator; no number; color/position only | Material 3, Polaris, Radix |
49
+ | Icon overlay | Small icon inside badge shape (rare) | Material 3 |
50
+
51
+ **Norm** (≥4/18 systems agree): count badges cap display at "99+"; zero count hidden by default; dot variant uses the same position/size slot as count but without text.
52
+ **Diverge**: Material 3 calls the dot variant "small badge" (8px, no content) vs. "large badge" (16px+, with number); Polaris uses "badge" exclusively for text status labels (not overlaid); Carbon uses "tag" for text labels, "notification badge" for counts. Radix provides a headless primitive suitable for all variants.
53
+
54
+ ---
55
+
56
+ ## States
57
+
58
+ | State | Trigger | Visual | ARIA |
59
+ |-------|---------|--------|------|
60
+ | default | count > 0 | Badge visible, pill shape | Parent `aria-label` updated |
61
+ | zero | count = 0 | Badge hidden (default); visible if `showZero` prop | Parent `aria-label` reflects 0 or omits count |
62
+ | max overflow | count > 99 | Renders "99+" | Parent `aria-label` says "99 or more unread" |
63
+ | dot status | status active | Dot visible; colored by status | Parent `aria-label` includes status text |
64
+
65
+ ---
66
+
67
+ ## Sizing & Spacing
68
+
69
+ | Variant | Min height | Min width | Font size | Notes |
70
+ |---------|-----------|-----------|-----------|-------|
71
+ | Count (sm) | 16px | 16px (= height) | 10px/500 | Single digit fills circle |
72
+ | Count (md) | 20px | 20px (= height) | 12px/500 | Two-digit + "99+" |
73
+ | Dot | 8px | 8px | — | No text; absolute top-right |
74
+
75
+ - **Shape**: pill when width > height; circle when width = height (single digit)
76
+ - **Position** (attached): `position: absolute; top: -4px; right: -4px` relative to parent
77
+ - **Min-width = height** (pill rule): ensures pill does not compress on 2-digit counts
78
+
79
+ **Norm**: 16–20px height; 10–12px font-size; min-width equals height *(Material 3, Polaris, Carbon)*.
80
+
81
+ ---
82
+
83
+ ## Typography
84
+
85
+ - Count text: 10–12px / 500 (medium weight) — legible at small scale
86
+ - No wrapping; never truncate count text; use "99+" cap instead
87
+ - Letter-spacing: 0 — tight spacing at 10–12px scale
88
+
89
+ Cross-link: `reference/typography.md` — small label scale (10–12px)
90
+
91
+ ---
92
+
93
+ ## Keyboard & Accessibility
94
+
95
+ > **WAI-ARIA role**: none (decorative); parent element carries accessible count via `aria-label`
96
+ > **Required attributes**: none on badge itself; `aria-label` on parent with count embedded
97
+
98
+ ### Keyboard Contract
99
+
100
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/ — W3C — 2024*
101
+
102
+ | Key | Action |
103
+ |-----|--------|
104
+ | (none) | Badge is non-interactive; no keyboard behavior |
105
+
106
+ Badge never receives focus. It is a purely visual overlay. All keyboard interaction is on the parent element.
107
+
108
+ ### Accessibility Rules
109
+
110
+ - Badge element itself MUST have `aria-hidden="true"` — screen readers should not announce the badge number separately; they read it as part of the parent's `aria-label`
111
+ - Parent element MUST have an `aria-label` that includes the count: `aria-label="Messages, 3 unread"` *(WAI-ARIA APG, Material 3)*
112
+ - Parent `aria-label` MUST be updated dynamically when count changes — use `aria-live` on the parent if the count changes while the page is rendered *(WCAG 4.1.3)*
113
+ - Zero badge: if badge is hidden at zero, remove it from DOM (or `display: none`) — do NOT leave it with empty text in the DOM *(Polaris)*
114
+ - Dot variant: parent `aria-label` MUST include the status: `aria-label="User profile, status: online"` *(Radix)*
115
+ - Never rely on badge color alone to communicate status — include text in parent `aria-label` *(WCAG 1.4.1)*
116
+
117
+ Cross-link: `reference/accessibility.md` — dynamic label updates, aria-live
118
+
119
+ ---
120
+
121
+ ## Motion
122
+
123
+ | Transition | Duration | Easing | Notes |
124
+ |------------|----------|--------|-------|
125
+ | Count increment (scale pulse) | 150ms | ease-out | Brief scale 1.2 → 1.0 on value change |
126
+ | Badge appear | 100ms | ease-out | Fade + scale from 0.5 |
127
+ | Badge disappear (at zero) | 80ms | ease-in | Fade + scale to 0 |
128
+
129
+ **BAN**: Continuous bounce animation on badge — implies urgency and is distracting; one-shot pulse on value change is acceptable.
130
+
131
+ Cross-link: `reference/motion.md` — scale-in/scale-out; `prefers-reduced-motion`: skip all badge animation
132
+
133
+ ---
134
+
135
+ ## Do / Don't
136
+
137
+ ### Do
138
+ - Include count in parent `aria-label`: `aria-label="Inbox, 5 unread messages"` *(WAI-ARIA APG)*
139
+ - Add `aria-hidden="true"` to the badge element itself *(WAI-ARIA APG)*
140
+ - Cap numeric display at "99+" and update parent label to "99 or more" *(Material 3, Carbon)*
141
+ - Hide badge when count is 0 by default; offer `showZero` prop for product preference *(Polaris, Radix)*
142
+
143
+ ### Don't
144
+ - Don't surface badge count only through color (dot badge without parent label) *(WCAG 1.4.1)*
145
+ - Don't put interactive content in a badge — it is always decorative *(Material 3, Carbon)*
146
+ - Don't animate badge continuously — one-shot pulse on value change only *(Polaris)*
147
+ - Don't use badge text as the only accessible notification — always update parent `aria-label` *(WAI-ARIA APG)*
148
+
149
+ ---
150
+
151
+ ## Anti-patterns Cross-links
152
+
153
+ | Anti-pattern | Entry |
154
+ |--------------|-------|
155
+ | Color as sole status differentiator | `reference/anti-patterns.md#ban-color-only` |
156
+ | Missing aria-label on parent with count | `reference/anti-patterns.md#ban-aria-label` |
157
+
158
+ ---
159
+
160
+ ## Benchmark Citations
161
+
162
+ | Claim | Sources |
163
+ |-------|---------|
164
+ | Badge is decorative; parent carries aria-label | Material 3, Polaris, Carbon, Radix |
165
+ | Count capped at "99+" | Material 3, Carbon, Radix |
166
+ | 16–20px height; min-width = height | Material 3, Polaris, Carbon |
167
+ | Zero badge hidden by default | Polaris, Radix |
168
+ | aria-hidden="true" on badge element | WAI-ARIA APG |
169
+ | Parent aria-label updated on count change | WCAG 4.1.3 |
170
+
171
+ Full system URLs: `connections/design-corpora.md`
172
+
173
+ ---
174
+
175
+ ## Grep Signatures
176
+
177
+ ```bash
178
+ # Badge count not surfaced in parent aria-label
179
+ grep -rn 'badge\|Badge' src/ | grep -v 'aria-label' | grep -v 'aria-hidden'
180
+
181
+ # Badge element missing aria-hidden
182
+ grep -rn 'class.*badge\|className.*badge' src/ | grep -v 'aria-hidden="true"'
183
+
184
+ # Parent with badge but no aria-label
185
+ grep -rn 'badge\|Badge' src/ -l | xargs grep -l 'button\|icon\|avatar' | xargs grep -L 'aria-label'
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Failing Example
191
+
192
+ ```html
193
+ <!-- BAD: badge count visible but parent has no aria-label for screen readers -->
194
+ <button class="icon-btn">
195
+ <svg aria-hidden="true"><!-- bell icon --></svg>
196
+ <span class="badge">3</span>
197
+ </button>
198
+ ```
199
+
200
+ **Why it fails**: Screen readers announce "button" with no mention of the count. The `<span class="badge">3</span>` may be announced as isolated "3" out of context, or the badge text is read before the button label, producing confusing output. The button has no accessible name describing its purpose or the notification count.
201
+ **Grep detection**: `grep -rn 'class.*badge' src/ | xargs grep -B2 -A2 'button\|icon' | grep -v 'aria-label'`
202
+ **Fix**: `<button class="icon-btn" aria-label="Notifications, 3 unread"><svg aria-hidden="true">…</svg><span class="badge" aria-hidden="true">3</span></button>`
@@ -0,0 +1,198 @@
1
+ # Breadcrumbs — Benchmark Spec
2
+
3
+ **Harvested from**: WAI-ARIA APG, Carbon, Polaris, Material 3, Atlassian
4
+ **Wave**: 4 · **Category**: Navigation
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A breadcrumb trail shows a user's location within a hierarchical navigation structure, providing a supplemental path back to any ancestor page. It is not primary navigation — the current page is always the last item and is never a link. Breadcrumbs are most valuable in deep hierarchies (> 2 levels) and information-dense applications. *(WAI-ARIA APG breadcrumb pattern, Carbon, Polaris, Material 3, Atlassian all define breadcrumb as a supplemental location indicator)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ <nav aria-label="Breadcrumb">
18
+ <ol role="list">
19
+ <li><a href="/">Home</a></li>
20
+ <li><span aria-hidden="true">›</span><a href="/products">Products</a></li>
21
+ <li><span aria-hidden="true">›</span><span aria-current="page">Widget Pro</span></li>
22
+ </ol>
23
+ </nav>
24
+ ```
25
+
26
+ | Part | Required | Notes |
27
+ |------|----------|-------|
28
+ | `<nav>` container | Yes | `role="navigation"` + `aria-label="Breadcrumb"` |
29
+ | `<ol>` list | Yes | Ordered list — sequence matters |
30
+ | `<li>` items | Yes | One per level in the path |
31
+ | Ancestor links | Yes | `<a href>` pointing to each ancestor URL |
32
+ | Current page | Yes | Plain text `<span>` (not a link) + `aria-current="page"` |
33
+ | Separator | Yes | `aria-hidden="true"` character or SVG icon; never in link text |
34
+ | Ellipsis (deep paths) | No | Truncate middle items at > 4 levels; always keep first + last 2 |
35
+
36
+ ---
37
+
38
+ ## Variants
39
+
40
+ | Variant | Description | Systems |
41
+ |---------|-------------|---------|
42
+ | Default | Inline trail, all items visible | All systems |
43
+ | Collapsed / truncated | Middle items replaced by "…" at > 4 levels | Carbon, Polaris, Atlassian |
44
+ | With icons | Leading icon per item (folder/page icon) | Material 3, Atlassian |
45
+ | Large / heading-inline | Larger type, sits beside or below a page title | Polaris, Atlassian |
46
+
47
+ **Norm** (≥4 systems agree): separator is `aria-hidden`; current page uses `aria-current="page"`; last item is never a link.
48
+ **Diverge**: Polaris uses `>` as separator; Carbon uses `/`; Material 3 uses `›` (chevron). All are acceptable — choose to match your product's visual language.
49
+
50
+ ---
51
+
52
+ ## States
53
+
54
+ | State | Trigger | Visual | ARIA |
55
+ |-------|---------|--------|------|
56
+ | default | — | Ancestor links + muted separator + current page text | — |
57
+ | link-hover | pointer over ancestor | Underline or color shift | — |
58
+ | link-focus | keyboard focus on ancestor | 2px focus-visible ring | — |
59
+ | current | — | No underline; muted or bold treatment; not clickable | `aria-current="page"` |
60
+ | truncated | path > 4 levels | Middle items hidden; "…" button shown | `aria-label="Show full path"` on ellipsis button |
61
+
62
+ ---
63
+
64
+ ## Sizing & Spacing
65
+
66
+ | Element | Value | Notes |
67
+ |---------|-------|-------|
68
+ | Item font size | 13–14px (body-sm) | Smaller than page headings; supplemental context |
69
+ | Item height | 24–32px | Allow for comfortable touch targets on mobile |
70
+ | Separator spacing | 4–8px H padding each side | Optical breathing room |
71
+ | Max visible levels | 4 | Truncate middle items beyond 4 levels |
72
+ | Truncation rule | Keep: first + last 2 items | Never truncate the current page or the root |
73
+
74
+ **Norm**: 13–14px font size (Carbon, Polaris, Atlassian); items on a single line; no wrapping.
75
+
76
+ ---
77
+
78
+ ## Typography
79
+
80
+ - Ancestor links: body-sm, weight 400, `color: --text-link`; underline on hover
81
+ - Current page: body-sm, weight 500 or 600 (bold for emphasis), `color: --text-primary`; no underline
82
+ - Separator: body-sm, `color: --text-subtle`, `aria-hidden="true"`
83
+ - Do not use uppercase or letter-spacing on breadcrumb items — they should read as natural path segments
84
+
85
+ Cross-link: `reference/typography.md` — body-sm, link color tokens
86
+
87
+ ---
88
+
89
+ ## Keyboard & Accessibility
90
+
91
+ > **WAI-ARIA role**: `navigation` (container)
92
+ > **Required attributes**: `aria-label="Breadcrumb"` on `<nav>`; `aria-current="page"` on last item; `aria-hidden="true"` on separators
93
+
94
+ ### Keyboard Contract
95
+
96
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/ — W3C — 2024*
97
+
98
+ | Key | Action |
99
+ |-----|--------|
100
+ | Tab | Moves focus to next focusable link in the breadcrumb trail |
101
+ | Shift+Tab | Moves focus to previous link |
102
+ | Enter | Activates the focused link (navigates to ancestor) |
103
+
104
+ ### Accessibility Rules
105
+
106
+ - `aria-current="page"` MUST be on the last item (current page) — this is the primary AT signal
107
+ - Separators MUST be `aria-hidden="true"` — screen readers should announce "Home, Products, Widget Pro" not "Home › Products › Widget Pro"
108
+ - The current page item MUST NOT be a link — it is the user's current location
109
+ - `<ol>` conveys that order matters; do not use `<ul>` or a `<div>` list
110
+ - `aria-label="Breadcrumb"` distinguishes this nav from primary/secondary navigation landmarks
111
+
112
+ Cross-link: `reference/accessibility.md` — aria-current, landmark labelling
113
+
114
+ ---
115
+
116
+ ## Motion
117
+
118
+ | Transition | Duration | Easing | Notes |
119
+ |------------|----------|--------|-------|
120
+ | Ellipsis expand | 150ms | ease-out | Reveal hidden items inline |
121
+ | Ellipsis collapse | 120ms | ease-in | Hide middle items |
122
+
123
+ **BAN**: Do not animate the breadcrumb trail on route change — route transitions are the page's responsibility, not the breadcrumb's.
124
+
125
+ Cross-link: `reference/motion.md` — layout transitions
126
+
127
+ ---
128
+
129
+ ## Do / Don't
130
+
131
+ ### Do
132
+ - Use `<ol>` for the list — order is meaningful in a hierarchical path *(WAI-ARIA APG)*
133
+ - Mark the current page with `aria-current="page"` — not just a CSS class *(WAI-ARIA APG, Atlassian)*
134
+ - Hide separators with `aria-hidden="true"` *(WAI-ARIA APG, Carbon, Polaris)*
135
+ - Truncate middle items (not first/last) when path exceeds 4 levels *(Carbon, Polaris, Atlassian)*
136
+
137
+ ### Don't
138
+ - Don't make the current page item a link — it creates a circular self-referential link *(WAI-ARIA APG)*
139
+ - Don't include separator text inside link labels — `aria-hidden` the separator element, not the link *(WAI-ARIA APG)*
140
+ - Don't use breadcrumbs as the only navigation method — they supplement; primary nav is required *(Material 3, Polaris)*
141
+ - Don't truncate the root or current page items — users need anchoring context at both ends *(Carbon, Atlassian)*
142
+
143
+ ---
144
+
145
+ ## Anti-patterns Cross-links
146
+
147
+ | Anti-pattern | Entry |
148
+ |--------------|-------|
149
+ | BAN-07 | Missing `aria-current` on active navigation items — `reference/anti-patterns.md#ban-07` |
150
+
151
+ ---
152
+
153
+ ## Benchmark Citations
154
+
155
+ | Claim | Sources |
156
+ |-------|---------|
157
+ | aria-label="Breadcrumb" on nav container | WAI-ARIA APG breadcrumb pattern |
158
+ | Separator must be aria-hidden | WAI-ARIA APG, Carbon, Polaris, Atlassian |
159
+ | Last item not a link + aria-current="page" | WAI-ARIA APG, Carbon, Polaris, Material 3 |
160
+ | Ordered list (`<ol>`) for hierarchy | WAI-ARIA APG |
161
+ | Truncate middle at > 4 levels | Carbon, Polaris, Atlassian |
162
+
163
+ Full system URLs: `connections/design-corpora.md`
164
+
165
+ ---
166
+
167
+ ## Grep Signatures
168
+
169
+ ```bash
170
+ # Last breadcrumb item is a link (should be plain text with aria-current)
171
+ grep -rn 'breadcrumb\|bread-crumb' src/ | grep -v 'aria-current="page"'
172
+
173
+ # Separator not aria-hidden
174
+ grep -rn 'breadcrumb' src/ | grep '›\|/\|>\|chevron' | grep -v 'aria-hidden'
175
+
176
+ # Using <ul> instead of <ol> for breadcrumb
177
+ grep -rn 'breadcrumb' src/ | grep '<ul'
178
+
179
+ # nav container missing aria-label
180
+ grep -rn 'breadcrumb' src/ | grep '<nav' | grep -v 'aria-label'
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Failing Example
186
+
187
+ ```html
188
+ <!-- BAD: all items are links including the current page, separators not hidden -->
189
+ <nav> <!-- missing aria-label="Breadcrumb" -->
190
+ <a href="/">Home</a> /
191
+ <a href="/products">Products</a> /
192
+ <a href="/products/widget-pro">Widget Pro</a> <!-- should NOT be a link; no aria-current -->
193
+ </nav>
194
+ ```
195
+
196
+ **Why it fails**: Current page is a link (circular/confusing); separators `/` are announced by screen readers; `<nav>` has no label; no `aria-current="page"` to signal location to AT users; no `<ol>/<li>` structure loses hierarchy semantics.
197
+ **Grep detection**: `grep -rn 'breadcrumb' src/ | grep -v 'aria-current'`
198
+ **Fix**: Replace last `<a>` with `<span aria-current="page">Widget Pro</span>`; wrap separators in `<span aria-hidden="true">/</span>`; add `aria-label="Breadcrumb"` to `<nav>`; wrap items in `<ol><li>`.
@@ -0,0 +1,195 @@
1
+ # Button — Benchmark Spec
2
+
3
+ **Harvested from**: Material 3, Polaris, Carbon, Fluent 2, Radix, shadcn/ui, Primer, Atlassian
4
+ **Wave**: 1 · **Category**: Inputs
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A button triggers a discrete action in the current context — submitting a form, opening a dialog, executing a command. It is not a navigation element (use Link for href-based navigation). Buttons have an explicit visual affordance of clickability and must communicate their current state (loading, disabled) clearly. *(Material 3, Carbon, Polaris agree: button = action trigger, not navigation)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ [ icon? ] [ label ] [ trailing-icon? ]
18
+ └── role="button" or <button>
19
+ └── focus-visible ring (2px offset)
20
+ ```
21
+
22
+ | Part | Required | Notes |
23
+ |------|----------|-------|
24
+ | Label | Yes (or `aria-label`) | Visible text preferred; `aria-label` for icon-only |
25
+ | Root element | Yes | Must be `<button>` or element with `role="button"` + `tabindex="0"` |
26
+ | Leading icon | No | Left-aligned, 16–20px, optical spacing ~8px |
27
+ | Trailing icon | No | Right-aligned; use sparingly (chevron for split buttons) |
28
+ | Focus ring | Yes | 2px solid, 2px offset from border; never hidden |
29
+ | Loading indicator | No | Spinner replaces or overlays label; `aria-busy="true"` |
30
+
31
+ ---
32
+
33
+ ## Variants
34
+
35
+ | Variant | Description | Systems |
36
+ |---------|-------------|---------|
37
+ | Primary / Filled | Highest emphasis; one per view section | Material 3, Carbon, Polaris, Fluent, Primer |
38
+ | Secondary / Outlined | Medium emphasis; alternate action | Material 3, Carbon, Polaris, Fluent |
39
+ | Ghost / Text | Low emphasis; tertiary or inline actions | Material 3 (text), Carbon (ghost), Polaris (plain) |
40
+ | Destructive | Irreversible actions (delete, remove) | Polaris (critical), Carbon (danger), shadcn |
41
+ | Icon-only | No visible label; requires `aria-label` | All systems |
42
+ | Link-style | Looks like link, behaves as button | Carbon, Primer |
43
+
44
+ **Norm** (≥6/18 systems agree): primary/secondary/ghost hierarchy; one primary per viewport section.
45
+ **Diverge**: "tertiary" naming (Material 3) vs. "ghost" (Carbon) vs. "plain" (Polaris) — same visual intent, different labels.
46
+
47
+ ---
48
+
49
+ ## States
50
+
51
+ | State | Trigger | Visual | ARIA |
52
+ |-------|---------|--------|------|
53
+ | default | — | Resting fill/border | — |
54
+ | hover | pointer over | 8% overlay (light) / 8% overlay (dark) | — |
55
+ | focus | keyboard focus | 2px focus-visible ring, 2px offset | — |
56
+ | active / pressed | mousedown / Space / Enter | 12% overlay; scale 0.96 | — |
57
+ | disabled | `disabled` attr | 38% opacity; cursor: not-allowed | `disabled` attr |
58
+ | loading | async action in flight | Spinner, `aria-busy="true"` | `aria-busy="true"` |
59
+
60
+ **Norm**: 96% scale on press (Material 3, shadcn, Carbon confirm). 38% opacity for disabled (Material 3 spec).
61
+ **Diverge**: hover overlay vs. background tint — systems use either approach; overlay is more theme-portable.
62
+
63
+ ---
64
+
65
+ ## Sizing & Spacing
66
+
67
+ | Size | Height | Padding H | Min-width | Font |
68
+ |------|--------|-----------|-----------|------|
69
+ | sm | 32px | 12px | 64px | 13px/500 |
70
+ | md (default) | 40px | 16px | 80px | 14px/500 |
71
+ | lg | 48px | 24px | 96px | 16px/500 |
72
+
73
+ **Norm**: 40px default height (Carbon, Polaris, Fluent all confirm). Min-width prevents single-character buttons.
74
+ Cross-link: `reference/surfaces.md` — hit-area rule (minimum 44×44px accessible tap target via padding, not height).
75
+
76
+ ---
77
+
78
+ ## Typography
79
+
80
+ - Weight: 500 (medium) — not bold; distinguishes from body text without being heavy *(Material 3, Carbon)*
81
+ - Letter-spacing: +0.01em for sm/md, 0 for lg
82
+ - No text truncation — resize button or use icon-only variant; truncated button labels break affordance
83
+
84
+ Cross-link: `reference/typography.md` — tabular-nums rule (use on loading counters, not labels)
85
+
86
+ ---
87
+
88
+ ## Keyboard & Accessibility
89
+
90
+ > **WAI-ARIA role**: `button`
91
+ > **Required attributes**: none if `<button>`; `role="button"` + `tabindex="0"` if `<div>`/`<span>`
92
+
93
+ ### Keyboard Contract
94
+
95
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/button/ — W3C — 2024*
96
+
97
+ | Key | Action |
98
+ |-----|--------|
99
+ | Enter | Activates the button |
100
+ | Space | Activates the button |
101
+
102
+ ### Accessibility Rules
103
+
104
+ - Icon-only buttons MUST have `aria-label` or `aria-labelledby` — a tooltip is not a substitute
105
+ - Loading state: set `aria-busy="true"` and disable pointer events; announce completion via `aria-live`
106
+ - Disabled: use native `disabled` attribute (not `aria-disabled`) unless button must remain focusable for tooltip explanation
107
+ - Never use `<div>` or `<a>` as a button trigger without `role="button"` and keyboard handlers
108
+ - Focus ring must never be `outline: none` without a visible CSS custom alternative
109
+
110
+ Cross-link: `reference/accessibility.md`
111
+
112
+ ---
113
+
114
+ ## Motion
115
+
116
+ | Transition | Duration | Easing | Notes |
117
+ |------------|----------|--------|-------|
118
+ | hover overlay | 120ms | ease-out | Subtle; background/border only |
119
+ | press scale (0.96) | 80ms | ease-in | Immediate tactile feedback |
120
+ | loading spinner in | 150ms | ease-out | Replaces or overlays label |
121
+
122
+ **BAN**: `transition: all` — catches width change on loading and causes layout jank.
123
+
124
+ Cross-link: `reference/motion.md` — canonical scale-on-press 0.96, BAN-04 (`transition: all`)
125
+
126
+ ---
127
+
128
+ ## Do / Don't
129
+
130
+ ### Do
131
+ - Use one primary button per section of a view *(Material 3, Carbon, Polaris)*
132
+ - Write labels as verb phrases: "Save changes", "Delete account" *(Polaris content guidelines)*
133
+ - Provide `aria-label` for icon-only variants *(WAI-ARIA APG)*
134
+ - Maintain 8px minimum spacing between adjacent buttons *(Carbon, Fluent)*
135
+
136
+ ### Don't
137
+ - Don't use a button for navigation to another page — use `<a href>` (Link) *(Carbon, Primer)*
138
+ - Don't disable a button without explaining why — prefer showing error state after submission *(Polaris)*
139
+ - Don't use "Click here" or "Submit" as labels — be specific about the action *(Polaris, Carbon)*
140
+ - Don't truncate button labels — buttons must fit their content *(Fluent, Atlassian)*
141
+
142
+ ---
143
+
144
+ ## Anti-patterns Cross-links
145
+
146
+ | Anti-pattern | Entry |
147
+ |--------------|-------|
148
+ | BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
149
+
150
+ ---
151
+
152
+ ## Benchmark Citations
153
+
154
+ | Claim | Sources |
155
+ |-------|---------|
156
+ | 40px default height | Carbon, Polaris, Fluent 2 |
157
+ | 96% press scale | Material 3, shadcn, Carbon |
158
+ | One primary per section | Material 3, Carbon, Polaris, Fluent |
159
+ | Space/Enter activation | WAI-ARIA APG §4.2 |
160
+ | 38% opacity disabled | Material 3 |
161
+
162
+ Full system URLs: `connections/design-corpora.md`
163
+
164
+ ---
165
+
166
+ ## Grep Signatures
167
+
168
+ ```bash
169
+ # Icon-only button missing aria-label
170
+ grep -rn '<button' src/ | grep -v 'aria-label\|aria-labelledby' | grep 'icon\|svg'
171
+
172
+ # div/span used as button without role
173
+ grep -rn '<div\|<span' src/ | grep 'onClick\|on:click' | grep -v 'role="button"'
174
+
175
+ # transition: all on button (BAN-04)
176
+ grep -rn 'transition:\s*all' src/ | grep -i 'button\|btn'
177
+
178
+ # Missing focus-visible — outline: none without alternative
179
+ grep -rn 'outline:\s*none\|outline:\s*0' src/ | grep -i 'button\|btn\|focus'
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Failing Example
185
+
186
+ ```html
187
+ <!-- BAD: div used as button — no keyboard access, no role, no focus management -->
188
+ <div class="btn" onclick="handleClick()">
189
+ <svg><!-- icon --></svg>
190
+ </div>
191
+ ```
192
+
193
+ **Why it fails**: Not reachable by keyboard; no ARIA role; Space/Enter do nothing; no accessible name.
194
+ **Grep detection**: `grep -rn '<div.*onClick\|<div.*on:click' src/ | grep -v 'role='`
195
+ **Fix**: Use `<button type="button" aria-label="[action]">` with the icon inside.