@hegemonart/get-design-done 1.15.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +9 -5
- package/.claude-plugin/plugin.json +19 -5
- package/CHANGELOG.md +122 -0
- package/README.md +41 -0
- package/SKILL.md +4 -1
- package/agents/component-benchmark-harvester.md +112 -0
- package/agents/component-benchmark-synthesizer.md +88 -0
- package/agents/design-auditor.md +60 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +61 -0
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/connections/design-corpora.md +158 -0
- package/package.json +13 -3
- package/reference/components/README.md +94 -0
- package/reference/components/TEMPLATE.md +184 -0
- package/reference/components/accordion.md +217 -0
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -0
- package/reference/components/button.md +195 -0
- package/reference/components/card.md +200 -0
- package/reference/components/checkbox.md +207 -0
- package/reference/components/chip.md +209 -0
- package/reference/components/command-palette.md +228 -0
- package/reference/components/date-picker.md +227 -0
- package/reference/components/drawer.md +201 -0
- package/reference/components/file-upload.md +219 -0
- package/reference/components/input.md +208 -0
- package/reference/components/label.md +200 -0
- package/reference/components/link.md +193 -0
- package/reference/components/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/modal-dialog.md +210 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/popover.md +197 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/radio.md +203 -0
- package/reference/components/rich-text-editor.md +226 -0
- package/reference/components/select-combobox.md +219 -0
- package/reference/components/sidebar.md +211 -0
- package/reference/components/skeleton.md +197 -0
- package/reference/components/slider.md +208 -0
- package/reference/components/stepper.md +220 -0
- package/reference/components/switch.md +194 -0
- package/reference/components/table.md +229 -0
- package/reference/components/tabs.md +213 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tooltip.md +201 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/image-optimization.md +582 -0
- package/reference/motion-advanced.md +754 -0
- package/reference/motion-easings.md +381 -0
- package/reference/motion-interpolate.md +282 -0
- package/reference/motion-spring.md +234 -0
- package/reference/motion-transition-taxonomy.md +155 -0
- package/reference/motion.md +20 -0
- package/reference/output-contracts/motion-map.schema.json +135 -0
- package/reference/registry.json +285 -0
- package/reference/registry.schema.json +6 -1
- package/reference/variable-fonts-loading.md +532 -0
- package/scripts/lib/easings.cjs +280 -0
- package/scripts/lib/parse-contract.cjs +220 -0
- package/scripts/lib/spring.cjs +160 -0
- package/scripts/tests/test-motion-provenance.sh +64 -0
- package/skills/benchmark/SKILL.md +105 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Popover — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Radix UI Popover, Floating UI, Mantine, Atlassian, WAI-ARIA APG, shadcn/ui, Carbon, Material 3
|
|
4
|
+
**Wave**: 2 · **Category**: Containers
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A popover is an anchored overlay that appears beside a trigger element, containing richer content than a tooltip (interactive content, forms, menus). It is dismissed by clicking outside, pressing Escape, or activating a close button. Unlike a modal, it does not trap focus (unless it contains a form that requires isolation). *(Radix, Floating UI, WAI-ARIA APG differentiate popover from both tooltip and modal)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
[Trigger button]
|
|
18
|
+
│
|
|
19
|
+
▼ (arrow / caret — optional)
|
|
20
|
+
┌───────────────────────┐
|
|
21
|
+
│ Title (opt.) [✕] │ ← optional close button
|
|
22
|
+
│───────────────────────│
|
|
23
|
+
│ Content / form │
|
|
24
|
+
│ │
|
|
25
|
+
│ [Action] │
|
|
26
|
+
└───────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Part | Required | Notes |
|
|
30
|
+
|------|----------|-------|
|
|
31
|
+
| Trigger | Yes | Button or interactive element that opens the popover |
|
|
32
|
+
| Popover container | Yes | `role="dialog"` OR no role for non-modal content |
|
|
33
|
+
| Content | Yes | Can include interactive elements (forms, links, buttons) |
|
|
34
|
+
| Arrow pointer | No | 8–12px; indicates anchor relationship |
|
|
35
|
+
| Close button | No | When popover contains a form (escape is always active) |
|
|
36
|
+
| Backdrop | No | Popovers typically dismiss on outside-click, not backdrop |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Variants
|
|
41
|
+
|
|
42
|
+
| Variant | Description | Systems |
|
|
43
|
+
|---------|-------------|---------|
|
|
44
|
+
| Default | Anchored to trigger; dismisses on outside-click | All |
|
|
45
|
+
| With title | Title bar + close button for complex content | Radix, shadcn, Atlassian |
|
|
46
|
+
| Form popover | Contains inputs; focus trap optional | Radix, Mantine |
|
|
47
|
+
| Contextual menu | List of actions (see also: Dropdown Menu pattern) | Material 3, Carbon |
|
|
48
|
+
| Inline picker | Date, color, emoji picker | Material 3, Mantine |
|
|
49
|
+
|
|
50
|
+
**Norm** (≥5/18): Escape closes; outside-click closes; arrow pointer indicates anchor.
|
|
51
|
+
**Diverge**: focus trap — Radix Popover does NOT trap focus (non-modal); Radix Dialog DOES. Use Dialog-pattern when content isolation is required.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## States
|
|
56
|
+
|
|
57
|
+
| State | ARIA |
|
|
58
|
+
|-------|------|
|
|
59
|
+
| Closed | `aria-expanded="false"` on trigger |
|
|
60
|
+
| Open | `aria-expanded="true"` on trigger; `aria-controls="popover-id"` |
|
|
61
|
+
| Loading | `aria-busy="true"` on popover body |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Positioning
|
|
66
|
+
|
|
67
|
+
*Per Floating UI — https://floating-ui.com — MIT — 2024*
|
|
68
|
+
|
|
69
|
+
| Property | Recommended Value | Notes |
|
|
70
|
+
|----------|-------------------|-------|
|
|
71
|
+
| Placement | `bottom` (default), `top`, `left`, `right` | Auto-flip on viewport edge |
|
|
72
|
+
| Auto-flip | `flip()` middleware | Flips to opposite side when space is insufficient |
|
|
73
|
+
| Auto-shift | `shift()` middleware | Shifts along the axis to stay in viewport |
|
|
74
|
+
| Offset | 8px from trigger | Gap between trigger and popover edge |
|
|
75
|
+
| Arrow | `arrow()` middleware | CSS custom property `--arrow-x`, `--arrow-y` |
|
|
76
|
+
|
|
77
|
+
Positioning must update on scroll and resize (`autoUpdate` from Floating UI).
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Sizing & Spacing
|
|
82
|
+
|
|
83
|
+
| Property | Value | Notes |
|
|
84
|
+
|----------|-------|-------|
|
|
85
|
+
| Min width | 200px | Prevents content collapse |
|
|
86
|
+
| Max width | 360px | Wider → use modal instead |
|
|
87
|
+
| Padding | 16px | |
|
|
88
|
+
| Arrow size | 8×8px | |
|
|
89
|
+
| Border radius | Match design token | `reference/surfaces.md` |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Keyboard & Accessibility
|
|
94
|
+
|
|
95
|
+
> **WAI-ARIA role**: `dialog` (when content requires focus isolation) OR no specific role
|
|
96
|
+
> **Trigger attributes**: `aria-expanded`, `aria-controls` (popover id), `aria-haspopup="dialog"` (when role="dialog")
|
|
97
|
+
|
|
98
|
+
### Keyboard Contract
|
|
99
|
+
|
|
100
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/ (non-modal variant) — W3C — 2024*
|
|
101
|
+
|
|
102
|
+
| Key | Action |
|
|
103
|
+
|-----|--------|
|
|
104
|
+
| Escape | Closes the popover; returns focus to trigger |
|
|
105
|
+
| Tab (inside popover) | Moves focus through interactive elements inside popover |
|
|
106
|
+
| Tab (last element inside) | Closes popover; moves focus to next element after trigger |
|
|
107
|
+
|
|
108
|
+
### Accessibility Rules
|
|
109
|
+
|
|
110
|
+
- Trigger MUST have `aria-expanded` toggled on open/close
|
|
111
|
+
- Trigger MUST have `aria-controls` pointing to the popover's `id`
|
|
112
|
+
- When popover is dismissed, focus MUST return to the trigger element
|
|
113
|
+
- Non-modal popover: Tab MAY leave the popover (focus is not trapped)
|
|
114
|
+
- Modal popover (form isolation): add `role="dialog"` + `aria-modal="true"` + focus trap
|
|
115
|
+
- Popover with interactive content: do NOT use `role="tooltip"` — tooltip cannot contain interactive elements
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Motion
|
|
120
|
+
|
|
121
|
+
| Transition | Duration | Easing | Notes |
|
|
122
|
+
|------------|----------|--------|-------|
|
|
123
|
+
| Open | 120ms | ease-out | scale 0.95→1 + fade; origin at trigger |
|
|
124
|
+
| Close | 80ms | ease-in | fade only |
|
|
125
|
+
| Position update | 0ms | — | No animation on reposition (prevents jank on scroll) |
|
|
126
|
+
|
|
127
|
+
Cross-link: `reference/motion.md` — `AnimatePresence`, `data-state` trigger for CSS transitions
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Do / Don't
|
|
132
|
+
|
|
133
|
+
### Do
|
|
134
|
+
- Use Floating UI or equivalent for positioning — manual positioning breaks on scroll and viewport edge *(Radix, Mantine, shadcn)*
|
|
135
|
+
- Dismiss on outside-click AND Escape *(WAI-ARIA APG, Radix, all systems)*
|
|
136
|
+
- Return focus to trigger on close *(WAI-ARIA APG)*
|
|
137
|
+
- Auto-flip and auto-shift so popover stays in viewport *(Floating UI)*
|
|
138
|
+
|
|
139
|
+
### Don't
|
|
140
|
+
- Don't use `role="tooltip"` for popovers with interactive content — tooltip has a different contract *(WAI-ARIA APG)*
|
|
141
|
+
- Don't position with `position: absolute` without a Floating UI — it will misalign on scroll *(Floating UI docs)*
|
|
142
|
+
- Don't make popovers wider than 360px — use a modal for complex content *(Atlassian, Carbon)*
|
|
143
|
+
- Don't auto-open popovers on hover — use tooltip for hover-triggered content *(Radix, WAI-ARIA APG)*
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Anti-patterns Cross-links
|
|
148
|
+
|
|
149
|
+
| Anti-pattern | Entry |
|
|
150
|
+
|--------------|-------|
|
|
151
|
+
| role="tooltip" on interactive content | `reference/anti-patterns.md` |
|
|
152
|
+
| Manual absolute positioning | `reference/anti-patterns.md` |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Benchmark Citations
|
|
157
|
+
|
|
158
|
+
| Claim | Sources |
|
|
159
|
+
|-------|---------|
|
|
160
|
+
| Escape closes popover | WAI-ARIA APG, Radix, all systems |
|
|
161
|
+
| aria-expanded on trigger | WAI-ARIA APG |
|
|
162
|
+
| Floating UI for positioning | Radix, Mantine, shadcn |
|
|
163
|
+
| No focus trap (non-modal) | Radix Popover docs |
|
|
164
|
+
| 8px offset from trigger | Floating UI default |
|
|
165
|
+
|
|
166
|
+
Full system URLs: `connections/design-corpora.md`
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Grep Signatures
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Popover trigger missing aria-expanded
|
|
174
|
+
grep -rn 'popover\|Popover' src/ | grep 'trigger\|button' | grep -v 'aria-expanded'
|
|
175
|
+
|
|
176
|
+
# role="tooltip" used for interactive popover
|
|
177
|
+
grep -rn 'role="tooltip"' src/ | xargs grep -l 'button\|input\|<a ' 2>/dev/null
|
|
178
|
+
|
|
179
|
+
# Manual absolute positioning (no Floating UI)
|
|
180
|
+
grep -rn 'position:\s*absolute' src/ | grep -i 'popover\|dropdown\|overlay'
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Failing Example
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<!-- BAD: popover with role="tooltip" containing a button — wrong role, keyboard broken -->
|
|
189
|
+
<div role="tooltip" id="popover" style="position:absolute;top:40px">
|
|
190
|
+
<p>Quick actions</p>
|
|
191
|
+
<button onclick="doAction()">Apply</button>
|
|
192
|
+
</div>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Why it fails**: `role="tooltip"` cannot contain interactive elements per ARIA spec. The button is announced incorrectly. Escape may not close (tooltip close behavior differs from popover/dialog).
|
|
196
|
+
**Grep detection**: `grep -rn 'role="tooltip"' src/ | xargs grep -l 'button\|<a \|input' 2>/dev/null`
|
|
197
|
+
**Fix**: Use `role="dialog"` with `aria-modal="false"` (non-modal) for interactive popovers, or use Radix `<Popover>` which handles all ARIA and positioning automatically.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Progress — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon (ProgressIndicator), Polaris, Mantine
|
|
4
|
+
**Wave**: 3 · **Category**: Feedback
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A progress indicator communicates the status of an ongoing operation. Determinate variants show a known percentage of completion (0–100%); indeterminate variants signal ongoing work when duration is unknown. Linear bars are suited to step-based or file-based progress; circular (spinner-ring) variants are suited to inline or compact contexts. *(Material 3, Carbon, Polaris, Mantine agree: separate determinate vs. indeterminate; linear vs. circular)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
**Linear**
|
|
17
|
+
```
|
|
18
|
+
[track ──────────────────────────────────]
|
|
19
|
+
[fill ◼◼◼◼◼◼◼◼◼◼◼◼◼◼·····················]
|
|
20
|
+
↑ role="progressbar" aria-valuenow="45"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Circular**
|
|
24
|
+
```
|
|
25
|
+
╭──╮
|
|
26
|
+
╭ ╮
|
|
27
|
+
╰ ╯ ← SVG stroke-dashoffset ring
|
|
28
|
+
╰──╯
|
|
29
|
+
role="progressbar"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Part | Required | Notes |
|
|
33
|
+
|------|----------|-------|
|
|
34
|
+
| Track | Yes | Background rail (linear) or ring background (circular) |
|
|
35
|
+
| Fill / indicator | Yes | Foreground showing progress amount |
|
|
36
|
+
| Label (visually hidden ok) | Yes | `aria-label` or `aria-labelledby` — describes what is loading |
|
|
37
|
+
| Value text | No | Rendered percentage (e.g. "45%") — supplement to ARIA value |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Variants
|
|
42
|
+
|
|
43
|
+
| Variant | Description | Systems |
|
|
44
|
+
|---------|-------------|---------|
|
|
45
|
+
| Linear determinate | Bar fills left-to-right proportionally | Material 3, Carbon, Polaris, Mantine |
|
|
46
|
+
| Linear indeterminate | Bar animates in loop (shimmer or sweep) | Material 3, Carbon, Polaris, Mantine |
|
|
47
|
+
| Circular determinate | SVG ring fills by stroke-dashoffset | Material 3, Carbon, Mantine |
|
|
48
|
+
| Circular indeterminate | SVG ring rotates in loop | Material 3, Carbon, Mantine |
|
|
49
|
+
|
|
50
|
+
**Norm** (≥4/18 systems agree): `role="progressbar"` on all variants; `aria-valuenow` only on determinate; `aria-valuemin=0` + `aria-valuemax=100` always.
|
|
51
|
+
**Diverge**: Polaris calls the circular variant "Spinner" (single indeterminate state only); Material 3 distinguishes "linear progress indicator" and "circular progress indicator" as separate component families; Carbon offers multi-step linear progress ("ProgressStep") as a distinct component.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## States
|
|
56
|
+
|
|
57
|
+
| State | Trigger | Visual | ARIA |
|
|
58
|
+
|-------|---------|--------|------|
|
|
59
|
+
| determinate (0–100%) | known progress value | Fill width/stroke = percentage | `aria-valuenow={n}` |
|
|
60
|
+
| indeterminate | unknown duration | Looping animation | `aria-valuetext="Loading"` |
|
|
61
|
+
| complete | value reaches 100% | Full fill; brief hold before removal | `aria-valuenow="100"` |
|
|
62
|
+
| paused | operation suspended | Static fill; muted color | `aria-valuetext="Paused"` |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Sizing & Spacing
|
|
67
|
+
|
|
68
|
+
**Linear**
|
|
69
|
+
|
|
70
|
+
| Size | Height | Notes |
|
|
71
|
+
|------|--------|-------|
|
|
72
|
+
| sm | 4px (default) | Decorative; thin above content |
|
|
73
|
+
| md | 8px | Accessible minimum — recommended when bar is the primary indicator |
|
|
74
|
+
| lg | 12px | High-emphasis; file upload, step progress |
|
|
75
|
+
|
|
76
|
+
**Circular**
|
|
77
|
+
|
|
78
|
+
| Size | Diameter | Stroke width | Notes |
|
|
79
|
+
|------|----------|-------------|-------|
|
|
80
|
+
| sm | 20px | 2px | Inline within text/button |
|
|
81
|
+
| md | 32px | 3px | Component-level loading |
|
|
82
|
+
| lg | 48px | 4px | Page/section loading |
|
|
83
|
+
|
|
84
|
+
**Norm**: 4px linear height default (Material 3); 8px recommended for standalone accessibility *(Carbon)*; circular diameter 20–48px *(Material 3, Mantine)*.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Typography
|
|
89
|
+
|
|
90
|
+
- Value label (if shown): numeric-sm (12px/tabular-nums) — percentage readability
|
|
91
|
+
- Associated label (if visible): body-sm (14px/400) — describes what is loading
|
|
92
|
+
- Do not truncate the associated label; use a visually-hidden version if space is constrained
|
|
93
|
+
|
|
94
|
+
Cross-link: `reference/typography.md` — tabular-nums for percentage values
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Keyboard & Accessibility
|
|
99
|
+
|
|
100
|
+
> **WAI-ARIA role**: `progressbar`
|
|
101
|
+
> **Required attributes**: `aria-valuemin="0"`, `aria-valuemax="100"`, `aria-label` or `aria-labelledby`; `aria-valuenow` on determinate only
|
|
102
|
+
|
|
103
|
+
### Keyboard Contract
|
|
104
|
+
|
|
105
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/meter/ — W3C — 2024*
|
|
106
|
+
|
|
107
|
+
| Key | Action |
|
|
108
|
+
|-----|--------|
|
|
109
|
+
| (none) | Progress bar is not interactive; no keyboard interaction required |
|
|
110
|
+
|
|
111
|
+
Progress indicators are read-only status elements. They receive no keyboard focus unless embedded in a larger focusable region.
|
|
112
|
+
|
|
113
|
+
### Accessibility Rules
|
|
114
|
+
|
|
115
|
+
- `aria-label` or `aria-labelledby` MUST describe what is loading (e.g. "Uploading file", "Loading results") — a bare `role="progressbar"` with no label is announced as empty *(WAI-ARIA APG)*
|
|
116
|
+
- Determinate bars MUST include `aria-valuenow` matching the current integer percentage *(WAI-ARIA APG)*
|
|
117
|
+
- Indeterminate bars MUST omit `aria-valuenow` and instead set `aria-valuetext="Loading"` or similar *(WAI-ARIA APG)*
|
|
118
|
+
- `aria-valuemin` and `aria-valuemax` MUST be present on all progress bars (default 0 and 100)
|
|
119
|
+
- Indeterminate animation MUST respect `prefers-reduced-motion` — reduce to opacity pulse or static indicator *(WCAG 2.3.3)*
|
|
120
|
+
- Color contrast of fill vs. track MUST meet 3:1 minimum for non-text UI components *(WCAG 1.4.11)*
|
|
121
|
+
|
|
122
|
+
Cross-link: `reference/accessibility.md` — `prefers-reduced-motion`, WCAG 1.4.11
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Motion
|
|
127
|
+
|
|
128
|
+
| Transition | Duration | Easing | Notes |
|
|
129
|
+
|------------|----------|--------|-------|
|
|
130
|
+
| Determinate fill advance | 300ms | ease-out | Smooth value update on change |
|
|
131
|
+
| Indeterminate linear sweep | 1.2s | ease-in-out | Infinite loop; reverse direction at 50% |
|
|
132
|
+
| Circular spin | 1.2s | linear | Single full rotation per cycle |
|
|
133
|
+
| Complete → remove | 400ms | ease-in | Brief hold at 100% then fade/collapse |
|
|
134
|
+
|
|
135
|
+
**BAN**: Bouncing or elasticity on indeterminate loop — communicates false progress rhythm. Do not use `transition: all` (catches color changes during theme swap).
|
|
136
|
+
|
|
137
|
+
Cross-link: `reference/motion.md` — `prefers-reduced-motion`: replace sweep with opacity 0.5→1 pulse
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Do / Don't
|
|
142
|
+
|
|
143
|
+
### Do
|
|
144
|
+
- Always provide `aria-label` describing what is loading *(WAI-ARIA APG)*
|
|
145
|
+
- Use `aria-valuenow` on determinate variants and omit on indeterminate *(WAI-ARIA APG)*
|
|
146
|
+
- Use 8px+ height for standalone linear bars — 4px bars lack sufficient touch and visual target *(Carbon)*
|
|
147
|
+
- Transition fill smoothly (300ms ease-out) when value updates *(Material 3, Mantine)*
|
|
148
|
+
|
|
149
|
+
### Don't
|
|
150
|
+
- Don't use `aria-valuenow` on indeterminate bars — it implies a known value *(WAI-ARIA APG)*
|
|
151
|
+
- Don't show a spinner (circular indeterminate) when content shape is known — use Skeleton instead *(Carbon, Polaris)*
|
|
152
|
+
- Don't remove the progress bar the instant it hits 100% — hold briefly so the completion is registered *(Material 3)*
|
|
153
|
+
- Don't animate with infinite bounce — implies bouncing progress rhythm *(Carbon, Mantine)*
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Anti-patterns Cross-links
|
|
158
|
+
|
|
159
|
+
| Anti-pattern | Entry |
|
|
160
|
+
|--------------|-------|
|
|
161
|
+
| Indeterminate bar with aria-valuenow | `reference/anti-patterns.md#ban-aria-value` |
|
|
162
|
+
| Spinner used when content shape is known | `reference/anti-patterns.md#ban-spinner-overuse` |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Benchmark Citations
|
|
167
|
+
|
|
168
|
+
| Claim | Sources |
|
|
169
|
+
|-------|---------|
|
|
170
|
+
| role="progressbar" on all variants | WAI-ARIA APG |
|
|
171
|
+
| aria-valuenow only on determinate | WAI-ARIA APG, Material 3, Carbon |
|
|
172
|
+
| aria-label required (what is loading) | WAI-ARIA APG |
|
|
173
|
+
| 8px accessible minimum height | Carbon |
|
|
174
|
+
| 1.2s animation loop duration | Material 3, Mantine |
|
|
175
|
+
| Respect prefers-reduced-motion | WCAG 2.3.3 |
|
|
176
|
+
|
|
177
|
+
Full system URLs: `connections/design-corpora.md`
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Grep Signatures
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Progress bar missing aria-label or aria-labelledby
|
|
185
|
+
grep -rn 'role="progressbar"' src/ | grep -v 'aria-label\|aria-labelledby'
|
|
186
|
+
|
|
187
|
+
# Determinate progress missing aria-valuenow
|
|
188
|
+
grep -rn 'role="progressbar"' src/ | grep -v 'aria-valuenow\|indeterminate'
|
|
189
|
+
|
|
190
|
+
# Progress missing valuemin/valuemax
|
|
191
|
+
grep -rn 'role="progressbar"' src/ | grep -v 'aria-valuemin\|aria-valuemax'
|
|
192
|
+
|
|
193
|
+
# Indeterminate with aria-valuenow (invalid pattern)
|
|
194
|
+
grep -rn 'indeterminate' src/ | grep 'aria-valuenow'
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Failing Example
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<!-- BAD: progress bar with no accessible label and no value attributes -->
|
|
203
|
+
<div class="progress-bar">
|
|
204
|
+
<div class="progress-bar__fill" style="width: 45%"></div>
|
|
205
|
+
</div>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Why it fails**: No `role="progressbar"` so screen readers do not recognize this as a progress indicator. No `aria-label` so there is no description of what is loading. No `aria-valuenow`, `aria-valuemin`, or `aria-valuemax` so screen readers cannot read the percentage even if the role were present.
|
|
209
|
+
**Grep detection**: `grep -rn 'progress-bar\|progressBar\|progress__fill' src/ | grep -v 'role="progressbar"'`
|
|
210
|
+
**Fix**: `<div role="progressbar" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100" aria-label="Uploading file"><div style="width:45%"></div></div>`
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Radio — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon, Fluent 2, Mantine, WAI-ARIA APG, Polaris, Ant Design, Chakra UI
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A radio button represents one option within a mutually exclusive group. Selecting one radio button deselects all others in the same group. Always wrap radio buttons in a `<fieldset>` + `<legend>` and group them with the same `name` attribute. Never use a single radio button — it creates an unresettable state. Use a checkbox for binary choices.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
<fieldset>
|
|
18
|
+
<legend>Shipping method</legend>
|
|
19
|
+
|
|
20
|
+
( ) Standard shipping ← <input type="radio" name="shipping" id="standard">
|
|
21
|
+
(●) Express shipping ← <input type="radio" name="shipping" id="express" checked>
|
|
22
|
+
( ) Overnight ← <input type="radio" name="shipping" id="overnight">
|
|
23
|
+
</fieldset>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Part | Required | Notes |
|
|
27
|
+
|------|----------|-------|
|
|
28
|
+
| `<fieldset>` | Yes | Groups the radio set |
|
|
29
|
+
| `<legend>` | Yes | Names the group; read by screen readers before each option |
|
|
30
|
+
| `<input type="radio">` | Yes | Native element; same `name` within group |
|
|
31
|
+
| `<label for="id">` | Yes | Click zone extends to label text |
|
|
32
|
+
| Helper text | No | Below individual item or below legend |
|
|
33
|
+
| Error message | Conditional | Group-level error via `aria-describedby` on `<fieldset>` |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Variants
|
|
38
|
+
|
|
39
|
+
| Variant | Description | Systems |
|
|
40
|
+
|---------|-------------|---------|
|
|
41
|
+
| Default (stacked) | Vertical list of options | All |
|
|
42
|
+
| Horizontal | Side-by-side for short labels (2–3 options) | Material 3, Carbon, Mantine |
|
|
43
|
+
| Radio card | Clickable card with radio indicator | Polaris, Mantine, shadcn |
|
|
44
|
+
| Button group | Segmented-control appearance | Material 3, Fluent 2, Carbon |
|
|
45
|
+
|
|
46
|
+
**Norm** (≥6/18): vertical stacking default; horizontal allowed for ≤3 short labels.
|
|
47
|
+
**Diverge**: button-group vs. radio-card — visual styling differs but keyboard contract is identical.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## States
|
|
52
|
+
|
|
53
|
+
| State | Trigger | Visual | ARIA |
|
|
54
|
+
|-------|---------|--------|------|
|
|
55
|
+
| unselected | default | Empty circle | — |
|
|
56
|
+
| selected | user interaction / programmatic | Filled dot | `checked` attr |
|
|
57
|
+
| hover | pointer over | Circle border darkens | — |
|
|
58
|
+
| focus | keyboard (Tab into group) | 2px focus ring on currently selected / first | — |
|
|
59
|
+
| disabled | `disabled` attr | 38% opacity | `aria-disabled="true"` |
|
|
60
|
+
| error | group-level | Red border on legend/container | `aria-describedby` on `<fieldset>` |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Sizing & Spacing
|
|
65
|
+
|
|
66
|
+
| Property | Value | Notes |
|
|
67
|
+
|----------|-------|-------|
|
|
68
|
+
| Control diameter | 16–20px | *(Material 3: 20px, Carbon: 16px, Polaris: 16px)* |
|
|
69
|
+
| Dot diameter | 8–10px (center of control) | |
|
|
70
|
+
| Gap: control → label | 8px | |
|
|
71
|
+
| Item spacing (vertical) | 8–12px | *(Carbon: 8px, Material 3: 8px)* |
|
|
72
|
+
| Touch target | 44×44px via pseudo-element | `reference/surfaces.md` |
|
|
73
|
+
|
|
74
|
+
Cross-link: `reference/surfaces.md` — hit-area pseudo-element pattern
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Typography
|
|
79
|
+
|
|
80
|
+
- Label: 14px/400 — same as body; option is a choice, not a heading
|
|
81
|
+
- Legend: 14px/500 or 12px/600 uppercase — distinct from option labels
|
|
82
|
+
- Helper/error: 12px/400
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Keyboard & Accessibility
|
|
87
|
+
|
|
88
|
+
> **WAI-ARIA role**: `radio` (implicit on `<input type="radio">`)
|
|
89
|
+
> **Group role**: `radiogroup` (via `<fieldset>` + ARIA, or `role="radiogroup"` explicitly)
|
|
90
|
+
> **Required attributes**: `name` (groups radios), `id` + `<label for>` per item
|
|
91
|
+
|
|
92
|
+
### Keyboard Contract
|
|
93
|
+
|
|
94
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/radio/ — W3C — 2024*
|
|
95
|
+
|
|
96
|
+
| Key | Action |
|
|
97
|
+
|-----|--------|
|
|
98
|
+
| Tab | Moves focus into group (to the checked radio, or first if none checked) |
|
|
99
|
+
| Tab (from within group) | Moves focus out of group to next focusable element |
|
|
100
|
+
| Arrow Right / Arrow Down | Moves focus to next radio AND selects it |
|
|
101
|
+
| Arrow Left / Arrow Up | Moves focus to previous radio AND selects it |
|
|
102
|
+
| Space | If the focused radio is not selected, selects it |
|
|
103
|
+
|
|
104
|
+
**Key insight**: Arrow keys move AND select simultaneously (auto-advance). This differs from checkboxes (Space toggles, Tab moves). Tab navigates in/out of the whole group as one focusable unit.
|
|
105
|
+
|
|
106
|
+
### Accessibility Rules
|
|
107
|
+
|
|
108
|
+
- `<fieldset>` + `<legend>` MUST wrap the entire group — legend text is prepended to each option announcement
|
|
109
|
+
- All radios in a group MUST share the same `name` attribute
|
|
110
|
+
- Tab focuses the selected radio (or first, if none selected) — arrow keys navigate within the group
|
|
111
|
+
- Never use a single radio button — it creates a state the user cannot unset; use a checkbox
|
|
112
|
+
- Error state applies to the group: `aria-describedby` on the `<fieldset>` or `role="radiogroup"` container
|
|
113
|
+
- `required`: apply to all radios in the group or use `aria-required` on the group container
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Motion
|
|
118
|
+
|
|
119
|
+
| Transition | Duration | Easing | Notes |
|
|
120
|
+
|------------|----------|--------|-------|
|
|
121
|
+
| Dot fill | 120ms | ease-out | Scale 0→1 from center |
|
|
122
|
+
| Deselect | 80ms | ease | Dot scale 1→0 |
|
|
123
|
+
| Hover border | 80ms | ease | Border colour only |
|
|
124
|
+
|
|
125
|
+
Cross-link: `reference/motion.md` — `prefers-reduced-motion`: skip dot animation, show fill instantly
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Do / Don't
|
|
130
|
+
|
|
131
|
+
### Do
|
|
132
|
+
- Always use `<fieldset>` + `<legend>` for the group *(WAI-ARIA APG, Carbon, Polaris)*
|
|
133
|
+
- Use the same `name` attribute for all radios in a group *(HTML spec, all systems)*
|
|
134
|
+
- Pre-select a default option where appropriate to reduce cognitive load *(Material 3, Polaris)*
|
|
135
|
+
- Use arrow keys to navigate within a group — Tab moves to the group, not each item *(WAI-ARIA APG)*
|
|
136
|
+
|
|
137
|
+
### Don't
|
|
138
|
+
- Don't use a single radio button — use a checkbox instead *(Material 3, Carbon, WAI-ARIA APG)*
|
|
139
|
+
- Don't use radio buttons for mutually exclusive options that require confirmation — use a select *(Polaris)*
|
|
140
|
+
- Don't mix radio and checkbox styles in the same group *(Carbon, Polaris)*
|
|
141
|
+
- Don't require the user to make a selection before seeing other page content (premature required state) *(Polaris)*
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Anti-patterns Cross-links
|
|
146
|
+
|
|
147
|
+
| Anti-pattern | Entry |
|
|
148
|
+
|--------------|-------|
|
|
149
|
+
| Lone radio button | `reference/anti-patterns.md` |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Benchmark Citations
|
|
154
|
+
|
|
155
|
+
| Claim | Sources |
|
|
156
|
+
|-------|---------|
|
|
157
|
+
| Arrow keys move + select simultaneously | WAI-ARIA APG §3.6 |
|
|
158
|
+
| Tab focuses whole group as one unit | WAI-ARIA APG |
|
|
159
|
+
| fieldset+legend required | WAI-ARIA APG, Carbon, Polaris |
|
|
160
|
+
| Single radio = anti-pattern | Material 3, Carbon, WAI-ARIA APG |
|
|
161
|
+
|
|
162
|
+
Full system URLs: `connections/design-corpora.md`
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Grep Signatures
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Single radio button (no group = unresettable state)
|
|
170
|
+
grep -rn 'type="radio"' src/ | sort | uniq -c | sort -n | head
|
|
171
|
+
|
|
172
|
+
# Radio group missing name attribute
|
|
173
|
+
grep -rn 'type="radio"' src/ | grep -v 'name='
|
|
174
|
+
|
|
175
|
+
# Radio group without fieldset or role="radiogroup"
|
|
176
|
+
grep -rn 'type="radio"' src/ | grep -v 'fieldset\|role="radiogroup"'
|
|
177
|
+
|
|
178
|
+
# Arrow key handler missing in custom radio implementation
|
|
179
|
+
grep -rn 'role="radio"' src/ | grep -v 'ArrowDown\|ArrowUp\|onKeyDown'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Failing Example
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
<!-- BAD: radio group without fieldset/legend and with keyboard nav broken -->
|
|
188
|
+
<div>
|
|
189
|
+
<input type="radio" id="a" name="plan"> <label for="a">Starter</label>
|
|
190
|
+
<input type="radio" id="b" name="plan"> <label for="b">Pro</label>
|
|
191
|
+
</div>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Why it fails**: Screen reader announces each option without the group question. The `<div>` provides no semantic grouping; `<legend>` is not present, so users don't know what "Starter" and "Pro" refer to.
|
|
195
|
+
**Grep detection**: `grep -B3 'type="radio"' src/ | grep '<div' | grep -v 'fieldset'`
|
|
196
|
+
**Fix**:
|
|
197
|
+
```html
|
|
198
|
+
<fieldset>
|
|
199
|
+
<legend>Select your plan</legend>
|
|
200
|
+
<input type="radio" id="a" name="plan"> <label for="a">Starter</label>
|
|
201
|
+
<input type="radio" id="b" name="plan"> <label for="b">Pro</label>
|
|
202
|
+
</fieldset>
|
|
203
|
+
```
|